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


Quelle  app-settings.test.ts

  Sprache: JAVA
 

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

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createStorageMock } from "../test-helpers/storage.ts";
import {
  applyResolvedTheme,
  applySettings,
  applySettingsFromUrl,
  setTabFromRoute,
  syncThemeWithSettings,
} from "./app-settings.ts";
import { normalizeImportedCustomTheme } from "./custom-theme.ts";
import type { ThemeMode, ThemeName } from "./theme.ts";

type Tab =
  | "agents"
  | "overview"
  | "channels"
  | "instances"
  | "sessions"
  | "usage"
  | "cron"
  | "skills"
  | "nodes"
  | "chat"
  | "config"
  | "communications"
  | "appearance"
  | "automation"
  | "infrastructure"
  | "aiAgents"
  | "debug"
  | "logs";

type SettingsHost = {
  settings: {
    gatewayUrl: string;
    token: string;
    sessionKey: string;
    lastActiveSessionKey: string;
    theme: ThemeName;
    themeMode: ThemeMode;
    chatFocusMode: boolean;
    chatShowThinking: boolean;
    chatShowToolCalls: boolean;
    splitRatio: number;
    navCollapsed: boolean;
    navWidth: number;
    navGroupsCollapsed: Record<string, boolean>;
    borderRadius: number;
    customTheme?: import("./custom-theme.ts").ImportedCustomTheme;
  };
  theme: ThemeName & ThemeMode;
  themeMode: ThemeMode;
  themeResolved: import("./theme.ts").ResolvedTheme;
  applySessionKey: string;
  sessionKey: string;
  tab: Tab;
  connected: boolean;
  chatHasAutoScrolled: boolean;
  logsAtBottom: boolean;
  eventLog: unknown[];
  eventLogBuffer: unknown[];
  basePath: string;
  themeMedia: MediaQueryList | null;
  themeMediaHandler: ((event: MediaQueryListEvent) => void) | null;
  logsPollInterval: number | null;
  debugPollInterval: number | null;
  pendingGatewayUrl?: string | null;
  pendingGatewayToken?: string | null;
  dreamingStatusLoading: boolean;
  dreamingStatusError: string | null;
  dreamingStatus: null;
  dreamingModeSaving: boolean;
  dreamDiaryLoading: boolean;
  dreamDiaryActionLoading: boolean;
  dreamDiaryActionMessage: { kind: "success" | "error"; text: string } | null;
  dreamDiaryActionArchivePath: string | null;
  dreamDiaryError: string | null;
  dreamDiaryPath: string | null;
  dreamDiaryContent: string | null;
  wikiImportInsightsLoading: boolean;
  wikiImportInsightsError: string | null;
  wikiImportInsights: null;
  wikiMemoryPalaceLoading: boolean;
  wikiMemoryPalaceError: string | null;
  wikiMemoryPalace: null;
};

function setTestWindowUrl(urlString: string) {
  const current = new URL(urlString);
  const history = {
    replaceState: vi.fn((_state: unknown, _title: string, nextUrl: string | URL) => {
      const next = new URL(String(nextUrl), current.toString());
      current.href = next.toString();
      current.protocol = next.protocol;
      current.host = next.host;
      current.pathname = next.pathname;
      current.search = next.search;
      current.hash = next.hash;
    }),
  };
  const locationLike = {
    get href() {
      return current.toString();
    },
    get protocol() {
      return current.protocol;
    },
    get host() {
      return current.host;
    },
    get pathname() {
      return current.pathname;
    },
    get search() {
      return current.search;
    },
    get hash() {
      return current.hash;
    },
  };
  vi.stubGlobal("window", {
    location: locationLike,
    history,
    setInterval,
    clearInterval,
  } as unknown as Window & typeof globalThis);
  vi.stubGlobal("location", locationLike as Location);
  return { history, location: locationLike };
}

const createHost = (tab: Tab): SettingsHost => ({
  settings: {
    gatewayUrl: "",
    token: "",
    sessionKey: "main",
    lastActiveSessionKey: "main",
    theme: "claw",
    themeMode: "system",
    chatFocusMode: false,
    chatShowThinking: true,
    chatShowToolCalls: true,
    splitRatio: 0.6,
    navCollapsed: false,
    navWidth: 220,
    navGroupsCollapsed: {},
    borderRadius: 50,
  },
  theme: "claw" as unknown as ThemeName & ThemeMode,
  themeMode: "system",
  themeResolved: "dark",
  applySessionKey: "main",
  sessionKey: "main",
  tab,
  connected: false,
  chatHasAutoScrolled: false,
  logsAtBottom: false,
  eventLog: [],
  eventLogBuffer: [],
  basePath: "",
  themeMedia: null,
  themeMediaHandler: null,
  logsPollInterval: null,
  debugPollInterval: null,
  pendingGatewayUrl: null,
  pendingGatewayToken: null,
  dreamingStatusLoading: false,
  dreamingStatusError: null,
  dreamingStatus: null,
  dreamingModeSaving: false,
  dreamDiaryLoading: false,
  dreamDiaryActionLoading: false,
  dreamDiaryActionMessage: null,
  dreamDiaryActionArchivePath: null,
  dreamDiaryError: null,
  dreamDiaryPath: null,
  dreamDiaryContent: null,
  wikiImportInsightsLoading: false,
  wikiImportInsightsError: null,
  wikiImportInsights: null,
  wikiMemoryPalaceLoading: false,
  wikiMemoryPalaceError: null,
  wikiMemoryPalace: null,
});

function createCustomThemeFixture() {
  return normalizeImportedCustomTheme(
    {
      name: "Light Green",
      cssVars: {
        theme: {
          "font-sans": "Inter, system-ui, sans-serif",
          "font-mono": "JetBrains Mono, monospace",
        },
        light: {
          background: "oklch(0.98 0.01 120)",
          foreground: "oklch(0.2 0.03 265)",
          card: "oklch(1 0 0)",
          "card-foreground": "oklch(0.2 0.03 265)",
          popover: "oklch(1 0 0)",
          "popover-foreground": "oklch(0.2 0.03 265)",
          primary: "oklch(0.8 0.2 128)",
          "primary-foreground": "oklch(0 0 0)",
          secondary: "oklch(0.35 0.03 257)",
          "secondary-foreground": "oklch(0.98 0.01 248)",
          muted: "oklch(0.96 0.01 248)",
          "muted-foreground": "oklch(0.55 0.04 257)",
          accent: "oklch(0.98 0.02 155)",
          "accent-foreground": "oklch(0.45 0.1 151)",
          destructive: "oklch(0.64 0.2 25)",
          "destructive-foreground": "oklch(1 0 0)",
          border: "oklch(0.92 0.01 255)",
          input: "oklch(0.92 0.01 255)",
          ring: "oklch(0.8 0.2 128)",
        },
        dark: {
          background: "oklch(0.12 0.04 265)",
          foreground: "oklch(0.98 0.01 248)",
          card: "oklch(0.2 0.04 266)",
          "card-foreground": "oklch(0.98 0.01 248)",
          popover: "oklch(0.2 0.04 266)",
          "popover-foreground": "oklch(0.98 0.01 248)",
          primary: "oklch(0.8 0.2 128)",
          "primary-foreground": "oklch(0 0 0)",
          secondary: "oklch(0.28 0.04 260)",
          "secondary-foreground": "oklch(0.98 0.01 248)",
          muted: "oklch(0.28 0.04 260)",
          "muted-foreground": "oklch(0.71 0.03 257)",
          accent: "oklch(0.39 0.09 152)",
          "accent-foreground": "oklch(0.8 0.2 128)",
          destructive: "oklch(0.44 0.16 27)",
          "destructive-foreground": "oklch(1 0 0)",
          border: "oklch(0.28 0.04 260)",
          input: "oklch(0.28 0.04 260)",
          ring: "oklch(0.8 0.2 128)",
        },
      },
    },
    {
      sourceUrl: "https://tweakcn.com/themes/cmlhfpjhw000004l4f4ax3m7z",
      themeId: "cmlhfpjhw000004l4f4ax3m7z",
    },
  );
}

describe("setTabFromRoute", () => {
  beforeEach(() => {
    vi.stubGlobal("localStorage", createStorageMock());
    vi.stubGlobal("navigator", { language: "en-US" } as Navigator);
  });

  afterEach(() => {
    vi.unstubAllGlobals();
  });

  it("starts and stops log polling based on the tab", () => {
    const host = createHost("chat");

    setTabFromRoute(host, "logs");
    expect(host.logsPollInterval).not.toBeNull();
    expect(host.debugPollInterval).toBeNull();

    setTabFromRoute(host, "chat");
    expect(host.logsPollInterval).toBeNull();
  });

  it("starts and stops debug polling based on the tab", () => {
    const host = createHost("chat");

    setTabFromRoute(host, "debug");
    expect(host.debugPollInterval).not.toBeNull();
    expect(host.logsPollInterval).toBeNull();

    setTabFromRoute(host, "chat");
    expect(host.debugPollInterval).toBeNull();
  });

  it("re-resolves the active palette when only themeMode changes", () => {
    const host = createHost("chat");
    host.settings.theme = "knot";
    host.settings.themeMode = "dark";
    host.theme = "knot" as unknown as ThemeName & ThemeMode;
    host.themeMode = "dark";
    host.themeResolved = "openknot";

    applySettings(host, {
      ...host.settings,
      themeMode: "light",
    });

    expect(host.theme).toBe("knot");
    expect(host.themeMode).toBe("light");
    expect(host.themeResolved).toBe("openknot-light");
  });

  it("syncs both theme family and mode from persisted settings", () => {
    const host = createHost("chat");
    host.settings.theme = "dash";
    host.settings.themeMode = "light";

    syncThemeWithSettings(host);

    expect(host.theme).toBe("dash");
    expect(host.themeMode).toBe("light");
    expect(host.themeResolved).toBe("dash-light");
  });

  it("falls back to claw when custom is selected without a stored custom theme", () => {
    const host = createHost("chat");
    host.settings.theme = "custom";
    host.settings.themeMode = "dark";

    syncThemeWithSettings(host);

    expect(host.theme).toBe("claw");
    expect(host.settings.theme).toBe("claw");
    expect(host.themeResolved).toBe("dark");
  });

  it("applies named system themes on OS preference changes", () => {
    const listeners: Array<(event: MediaQueryListEvent) => void> = [];
    const matchMedia = vi.fn().mockReturnValue({
      matches: false,
      addEventListener: (_name: string, handler: (event: MediaQueryListEvent) => void) => {
        listeners.push(handler);
      },
      removeEventListener: vi.fn(),
    });
    vi.stubGlobal("matchMedia", matchMedia);
    Object.defineProperty(window, "matchMedia", {
      configurable: true,
      value: matchMedia,
    });

    const host = createHost("chat");
    host.settings.theme = "knot" as unknown as ThemeName & ThemeMode;
    host.settings.themeMode = "system";

    syncThemeWithSettings(host);
    listeners[0]?.({ matches: true } as MediaQueryListEvent);
    expect(host.themeResolved).toBe("openknot");

    listeners[0]?.({ matches: false } as MediaQueryListEvent);
    expect(host.themeResolved).toBe("openknot");
  });

  it("normalizes light family themes to the shared light CSS token", () => {
    const root = {
      dataset: {} as DOMStringMap,
      style: { colorScheme: "" } as CSSStyleDeclaration & { colorScheme: string },
    };
    vi.stubGlobal("document", { documentElement: root } as Document);

    const host = createHost("chat");
    applyResolvedTheme(host, "dash-light");

    expect(host.themeResolved).toBe("dash-light");
    expect(root.dataset.theme).toBe("dash-light");
    expect(root.style.colorScheme).toBe("light");
  });

  it("applies imported custom light themes as light-mode tokens", () => {
    const root = {
      dataset: {} as DOMStringMap,
      style: { colorScheme: "" } as CSSStyleDeclaration & { colorScheme: string },
    };
    vi.stubGlobal("document", { documentElement: root } as Document);

    const host = createHost("chat");
    host.settings.customTheme = createCustomThemeFixture();
    applyResolvedTheme(host, "custom-light");

    expect(host.themeResolved).toBe("custom-light");
    expect(root.dataset.theme).toBe("custom-light");
    expect(root.style.colorScheme).toBe("light");
  });
});

describe("applySettingsFromUrl", () => {
  beforeEach(() => {
    vi.stubGlobal("localStorage", createStorageMock());
    vi.stubGlobal("sessionStorage", createStorageMock());
    vi.stubGlobal("navigator", { language: "en-US" } as Navigator);
    setTestWindowUrl("https://control.example/ui/overview");
  });

  afterEach(() => {
    vi.restoreAllMocks();
    vi.unstubAllGlobals();
  });

  it("hydrates query token params and strips them from the URL", () => {
    setTestWindowUrl("https://control.example/ui/overview?token=abc123&password=sekret");
    const host = createHost("overview");
    host.settings.gatewayUrl = "wss://control.example/openclaw";

    applySettingsFromUrl(host);

    expect(host.settings.token).toBe("abc123");
    expect(window.location.search).toBe("");
    expect(JSON.parse(localStorage.getItem("openclaw.control.settings.v1") ?? "{}").token).toBe(
      undefined,
    );
  });

  it("prefers fragment tokens over legacy query tokens when both are present", () => {
    setTestWindowUrl("https://control.example/ui/overview?token=query-token#token=hash-token");
    const host = createHost("overview");
    host.settings.gatewayUrl = "wss://control.example/openclaw";

    applySettingsFromUrl(host);

    expect(host.settings.token).toBe("hash-token");
    expect(window.location.search).toBe("");
    expect(window.location.hash).toBe("");
  });

  it("resets stale persisted session selection to main when a token is supplied without a session", () => {
    setTestWindowUrl("https://control.example/chat#token=test-token");
    const host = createHost("chat");
    host.settings = {
      ...host.settings,
      gatewayUrl: "ws://localhost:18789",
      token: "",
      sessionKey: "agent:test_old:main",
      lastActiveSessionKey: "agent:test_old:main",
    };
    host.sessionKey = "agent:test_old:main";

    applySettingsFromUrl(host);

    expect(host.sessionKey).toBe("main");
    expect(host.settings.sessionKey).toBe("main");
    expect(host.settings.lastActiveSessionKey).toBe("main");
  });

  it("characterizes token, session, and gateway URL combinations", () => {
    const scenarios = [
      {
        name: "same gateway applies token and session immediately",
        url: "https://control.example/chat?session=agent%3Atest_new%3Amain#token=token-a",
        settingsGatewayUrl: "ws://gateway-a.example:18789",
        settingsToken: "",
        expectedToken: "token-a",
        expectedSession: "agent:test_new:main",
        expectedPendingGatewayUrl: null,
        expectedPendingGatewayToken: null,
        expectedSearch: "?session=agent%3Atest_new%3Amain",
      },
      {
        name: "different gateway defers token and keeps explicit session",
        url: "https://control.example/chat?gatewayUrl=ws%3A%2F%2Fgateway-b.example%3A18789&session=agent%3Atest_new%3Amain#token=token-b",
        settingsGatewayUrl: "ws://gateway-a.example:18789",
        settingsToken: "",
        expectedToken: "",
        expectedSession: "agent:test_new:main",
        expectedPendingGatewayUrl: "ws://gateway-b.example:18789",
        expectedPendingGatewayToken: "token-b",
        expectedSearch: "?session=agent%3Atest_new%3Amain",
      },
      {
        name: "different gateway defers token without changing session",
        url: "https://control.example/chat?gatewayUrl=ws%3A%2F%2Fgateway-b.example%3A18789#token=token-c",
        settingsGatewayUrl: "ws://gateway-a.example:18789",
        settingsToken: "",
        expectedToken: "",
        expectedSession: "agent:test_old:main",
        expectedPendingGatewayUrl: "ws://gateway-b.example:18789",
        expectedPendingGatewayToken: "token-c",
        expectedSearch: "",
      },
      {
        name: "different gateway without token clears pending token",
        url: "https://control.example/chat?gatewayUrl=ws%3A%2F%2Fgateway-b.example%3A18789&session=agent%3Atest_new%3Amain",
        settingsGatewayUrl: "ws://gateway-a.example:18789",
        settingsToken: "existing-token",
        expectedToken: "existing-token",
        expectedSession: "agent:test_new:main",
        expectedPendingGatewayUrl: "ws://gateway-b.example:18789",
        expectedPendingGatewayToken: null,
        expectedSearch: "?session=agent%3Atest_new%3Amain",
      },
    ] as const;

    for (const scenario of scenarios) {
      setTestWindowUrl(scenario.url);
      const host = createHost("chat");
      host.settings = {
        ...host.settings,
        gatewayUrl: scenario.settingsGatewayUrl,
        token: scenario.settingsToken,
        sessionKey: "agent:test_old:main",
        lastActiveSessionKey: "agent:test_old:main",
      };
      host.sessionKey = "agent:test_old:main";

      applySettingsFromUrl(host);

      expect(host.settings.token, scenario.name).toBe(scenario.expectedToken);
      expect(host.sessionKey, scenario.name).toBe(scenario.expectedSession);
      expect(host.settings.sessionKey, scenario.name).toBe(scenario.expectedSession);
      expect(host.settings.lastActiveSessionKey, scenario.name).toBe(scenario.expectedSession);
      expect(host.pendingGatewayUrl, scenario.name).toBe(scenario.expectedPendingGatewayUrl);
      expect(host.pendingGatewayToken, scenario.name).toBe(scenario.expectedPendingGatewayToken);
      expect(window.location.search, scenario.name).toBe(scenario.expectedSearch);
      expect(window.location.hash, scenario.name).toBe("");
    }
  });
});

¤ Dauer der Verarbeitung: 0.27 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