Files
cc-switch/tests/lib/unsavedChanges.test.tsx
Owlbay a4d07f313c feat(ui): edit MCP and prompts in full pages and confirm before leaving unsaved editors (#7842)
MCP, prompt and Pi native prompt editors open as non-modal full pages (SheetPageContent) inside the content area, so the sidebar and window drag region stay usable. Provider editors drop the 1008px cap and follow the window width like the list pages.

Full-page editors register unsaved changes (input, change, and clicks on checkbox / switch / radio / option / aria-pressed controls; display-only toggles opt out with data-unsaved-ignore). Leaving through the sidebar, Cmd+, or the tray asks before discarding; back, cancel and Escape still close directly. Editor saves keep navigation locked via mutation keys.

Also truncates long page titles, makes the whole page header a drag region, and removes the editor delete buttons that no caller used.
2026-10-06 10:23:04 +08:00

96 lines
3.2 KiB
TypeScript

import { fireEvent, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it } from "vitest";
import { FullScreenPanel } from "@/components/common/FullScreenPanel";
import { Sheet, SheetPageContent } from "@/components/ui/sheet";
import {
discardUnsavedChanges,
hasUnsavedChanges,
useUnsavedChangesTracker,
} from "@/lib/unsavedChanges";
function Editor({ enabled = true }: { enabled?: boolean }) {
return (
<div {...useUnsavedChangesTracker(enabled)}>
<input aria-label="name" />
<button type="button" role="checkbox" aria-checked="false">
claude
</button>
<button type="button" aria-pressed="false" data-unsaved-ignore>
reveal
</button>
<button type="button">expand</button>
</div>
);
}
describe("unsaved changes tracker", () => {
afterEach(() => discardUnsavedChanges());
it("registers typing and edit controls, not view toggles or plain buttons", () => {
render(<Editor />);
fireEvent.click(screen.getByRole("button", { name: "reveal" }));
fireEvent.click(screen.getByRole("button", { name: "expand" }));
expect(hasUnsavedChanges()).toBe(false);
fireEvent.click(screen.getByRole("checkbox", { name: "claude" }));
expect(hasUnsavedChanges()).toBe(true);
});
it("registers text input and drops the entry on unmount", () => {
const view = render(<Editor />);
fireEvent.input(screen.getByRole("textbox", { name: "name" }), {
target: { value: "x" },
});
expect(hasUnsavedChanges()).toBe(true);
view.unmount();
expect(hasUnsavedChanges()).toBe(false);
});
it("does nothing while disabled and forgets edits once disabled", () => {
const view = render(<Editor enabled={false} />);
fireEvent.input(screen.getByRole("textbox", { name: "name" }));
expect(hasUnsavedChanges()).toBe(false);
view.rerender(<Editor />);
fireEvent.input(screen.getByRole("textbox", { name: "name" }));
expect(hasUnsavedChanges()).toBe(true);
view.rerender(<Editor enabled={false} />);
expect(hasUnsavedChanges()).toBe(false);
});
it("tracks FullScreenPanel only when asked and only while open", () => {
const panel = (props: { open: boolean; track: boolean }) => (
<FullScreenPanel
isOpen={props.open}
title="Edit"
onClose={() => undefined}
trackUnsavedChanges={props.track}
>
<input aria-label="field" />
</FullScreenPanel>
);
const view = render(panel({ open: true, track: false }));
fireEvent.input(screen.getByRole("textbox", { name: "field" }));
expect(hasUnsavedChanges()).toBe(false);
view.rerender(panel({ open: true, track: true }));
fireEvent.input(screen.getByRole("textbox", { name: "field" }));
expect(hasUnsavedChanges()).toBe(true);
view.rerender(panel({ open: false, track: true }));
expect(hasUnsavedChanges()).toBe(false);
});
it("tracks sheet editor pages", () => {
render(
<Sheet open modal={false}>
<SheetPageContent title="Edit MCP" closeLabel="back">
<textarea aria-label="body" />
</SheetPageContent>
</Sheet>,
);
fireEvent.input(screen.getByRole("textbox", { name: "body" }));
expect(hasUnsavedChanges()).toBe(true);
});
});