6715fbd971
- 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.
86 lines
2.6 KiB
TypeScript
86 lines
2.6 KiB
TypeScript
/**
|
|
* @file WebhookSettings.test.tsx
|
|
* @description The webhook form must not save "only selected rules" with no
|
|
* rule ticked (the server reads an empty list as ALL rules), and a failed
|
|
* toggle must surface as an error toast instead of only a console line.
|
|
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
|
*/
|
|
|
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
import { render, screen, waitFor } from "@testing-library/react";
|
|
import userEvent from "@testing-library/user-event";
|
|
|
|
const toastError = vi.fn();
|
|
vi.mock("../../lib/toast", () => ({
|
|
toast: { error: (m: unknown) => toastError(m), success: vi.fn(), info: vi.fn() },
|
|
}));
|
|
|
|
const update = vi.fn();
|
|
vi.mock("../../lib/api", () => ({
|
|
api: {
|
|
webhooks: {
|
|
list: vi.fn(async () => ({
|
|
targets: [
|
|
{
|
|
id: "w1",
|
|
name: "Team",
|
|
type: "generic",
|
|
url_preview: "https://x/…",
|
|
enabled: true,
|
|
rule_ids: [],
|
|
config: {},
|
|
},
|
|
],
|
|
})),
|
|
providers: vi.fn(async () => ({
|
|
providers: [
|
|
{
|
|
type: "generic",
|
|
label: "Generic",
|
|
url_required: true,
|
|
has_default_url: false,
|
|
supports_secret: false,
|
|
supports_headers: false,
|
|
fields: [],
|
|
},
|
|
],
|
|
})),
|
|
update: (...a: unknown[]) => update(...a),
|
|
create: vi.fn(),
|
|
remove: vi.fn(),
|
|
test: vi.fn(),
|
|
},
|
|
alerts: { rules: { list: vi.fn(async () => ({ rules: [{ id: "r1", name: "Rule one" }] })) } },
|
|
},
|
|
}));
|
|
|
|
import { WebhookSettings } from "../WebhookSettings";
|
|
|
|
beforeEach(() => {
|
|
toastError.mockReset();
|
|
update.mockReset();
|
|
});
|
|
|
|
describe("WebhookSettings", () => {
|
|
it("blocks 'only selected rules' with none ticked", async () => {
|
|
render(<WebhookSettings />);
|
|
await userEvent.click(await screen.findByRole("button", { name: /add/i }));
|
|
const inputs = screen.getAllByRole("textbox");
|
|
await userEvent.type(inputs[0]!, "Hook");
|
|
await userEvent.type(inputs[1]!, "https://example.com/h");
|
|
const create = screen.getByRole("button", { name: /^create/i });
|
|
expect(create).toBeEnabled();
|
|
await userEvent.click(screen.getByRole("checkbox", { name: /all/i }));
|
|
expect(create).toBeDisabled();
|
|
await userEvent.click(screen.getByRole("checkbox", { name: "Rule one" }));
|
|
expect(create).toBeEnabled();
|
|
});
|
|
|
|
it("toasts a failed toggle", async () => {
|
|
update.mockRejectedValue(new Error("boom"));
|
|
render(<WebhookSettings />);
|
|
await userEvent.click(await screen.findByRole("switch"));
|
|
await waitFor(() => expect(toastError).toHaveBeenCalled());
|
|
});
|
|
});
|