feat(settings): data-safe editors, deep-linked search, Data/About/Cost pages
- EditorModal loads once per path (a parent re-render no longer reloads the file over the user's typing), refuses Save after a failed load, opens a truncated (>256 KB) file read-only, and asks before overwriting a file that changed on disk or could not be re-read. - Settings/Hooks editors unset only keys from the snapshot taken at Edit, warn when the file changed underneath, keep a renamed key's value, and refuse a new key named like a redacted one (env) or a read-only hook event, which used to replace the secret/hooks. - Project scope shows its resolved folder and is disabled inside the plugin cache. Account-profile, pricing-row and pet deletes confirm; two-click actions disarm after 3 s; unsaved drafts ask before closing. - Search indexes every Claude Code / Codex sub-tab, opens it via ?tab= and scrolls to + highlights the row; the active config tab is in the URL. - General splits into General / Data / About; Pricing moves to a Cost group with one shared load; config tabs group as Content / Extensions / Config with arrow-key roving; Promise.allSettled per tab. - Data: browser-preferences export/import (allowlisted keys), per-section reset, housekeeping dry-run first, clear-all behind a typed phrase. Server: purge days must be a whole number >= 1 (INVALID_PURGE_DAYS) and dry_run returns exactly what the real run would delete. - Alerts can be edited; webhooks/alerts/remote-source failures toast; copies go through copyText; modals get dialog roles and useDialog. - Sagi: Ctrl/Cmd+B only when shown and not typing, default spot clear of row controls, arrow keys move it, reduced motion honoured; the dead /run launch link goes to the workspace.
This commit is contained in:
@@ -7,10 +7,11 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { render, screen, waitFor } from "@testing-library/react";
|
||||
import { render, screen, waitFor, within } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { AiRouterConfig } from "../AiRouterConfig";
|
||||
import { api } from "../../lib/api";
|
||||
import { settleConfirm, subscribeConfirm, type PendingConfirm } from "../../lib/confirm";
|
||||
|
||||
vi.mock("../../lib/api", () => ({
|
||||
api: {
|
||||
@@ -158,8 +159,18 @@ describe("AiRouterConfig", () => {
|
||||
await user.type(screen.getByLabelText("aiRouter.fieldModel"), "x");
|
||||
await user.click(screen.getByRole("button", { name: "aiRouter.save" }));
|
||||
|
||||
expect(await screen.findByText(/must be http/)).toBeInTheDocument();
|
||||
// Inside the dialog, not behind its scrim.
|
||||
const dialog = screen.getByRole("dialog");
|
||||
expect(await within(dialog).findByText(/must be http/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("closes the form on Escape", async () => {
|
||||
render(<AiRouterConfig />);
|
||||
const user = userEvent.setup();
|
||||
await user.click(await screen.findByRole("button", { name: /aiRouter\.add/ }));
|
||||
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
||||
await user.keyboard("{Escape}");
|
||||
await waitFor(() => expect(screen.queryByRole("dialog")).toBeNull());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -236,7 +247,8 @@ describe("AiRouterConfig — testing an endpoint before saving it", () => {
|
||||
|
||||
it("opens a profile's Edit and Remove from a right-click; the built-in row has no menu", async () => {
|
||||
vi.mocked(api.aiRouters.list).mockResolvedValue({ items: [native, nineRouter] });
|
||||
const confirm = vi.spyOn(window, "confirm").mockReturnValueOnce(false);
|
||||
let pending: PendingConfirm | null = null;
|
||||
const off = subscribeConfirm((p) => (pending = p));
|
||||
const user = userEvent.setup();
|
||||
render(<AiRouterConfig />);
|
||||
const row = (await screen.findByText("9Router")).closest("tr")!;
|
||||
@@ -246,12 +258,14 @@ describe("AiRouterConfig — testing an endpoint before saving it", () => {
|
||||
|
||||
await user.pointer({ keys: "[MouseRight]", target: row });
|
||||
await user.click(screen.getByRole("menuitem", { name: "aiRouter.remove" }));
|
||||
expect(confirm).toHaveBeenCalledWith("aiRouter.removeConfirm:9Router");
|
||||
await waitFor(() => expect(pending?.title).toBe("aiRouter.removeConfirm:9Router"));
|
||||
settleConfirm(false);
|
||||
await waitFor(() => expect(pending).toBeNull());
|
||||
expect(api.aiRouters.remove).not.toHaveBeenCalled();
|
||||
|
||||
await user.pointer({ keys: "[MouseRight]", target: row });
|
||||
await user.click(screen.getByRole("menuitem", { name: "aiRouter.edit" }));
|
||||
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
||||
confirm.mockRestore();
|
||||
off();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user