Files
cc-switch/tests/components/ClaudeStackModelsField.test.tsx
T
Jason f724474590 feat(forms): pick fetched models in a dialog that can also remove them
The fetched-model picker was an inline box under the model list that could
only add models: configured ones showed as checked and disabled. It is now
a dialog opened from the list header (Claude Code Aggregation, both Codex
model list layouts, OpenCode and MiniMax Code). A successful fetch opens it;
after that the button reads "Select models" and opens it without fetching
again, with a refresh button inside.

A checkbox now means "in the list": configured models start checked and
unchecking removes their rows on Apply, while Cancel changes nothing. Rows
that stay keep their order and fields, new picks are appended and get their
parameters filled. Configured models the provider did not return are listed
first in their own group so they can be removed too. In Codex, removing the
default model's row moves the default to the new first row, as deleting
the row does. The dialog has search, select all / none for the visible
models, a selected-only filter and a selected count.

Replaces fetchedModelsTitle, modelAlreadyAdded and addSelectedModels with
providerForm.modelPicker.*.
2026-10-11 22:51:29 +08:00

67 lines
2.2 KiB
TypeScript

import { fireEvent, screen, within } from "@testing-library/react";
import { renderWithQueryClient as render } from "../utils/testQueryClient";
import { useForm } from "react-hook-form";
import type { PropsWithChildren } from "react";
import { describe, expect, it, vi } from "vitest";
import {
ClaudeStackModelsField,
createClaudeStackModelRow,
} from "@/components/providers/forms/ClaudeStackModelsField";
import { Form } from "@/components/ui/form";
const FormShell = ({ children }: PropsWithChildren) => {
const form = useForm();
return <Form {...form}>{children}</Form>;
};
describe("ClaudeStackModelsField model picker", () => {
it("keeps checked rows untouched, removes unchecked ones and appends new picks", () => {
const first = createClaudeStackModelRow({
model: "glm-5.2[1m]",
displayName: "GLM",
});
const second = createClaudeStackModelRow({ model: "kimi-k3" });
const draft = createClaudeStackModelRow();
const onRowsChange = vi.fn();
render(
<FormShell>
<ClaudeStackModelsField
rows={[first, second, draft]}
onRowsChange={onRowsChange}
fetchedModels={[
{ id: "glm-5.2", ownedBy: null },
{ id: "kimi-k3", ownedBy: null },
{ id: "minimax-m3", ownedBy: null },
]}
onFetchModels={vi.fn()}
isFetchingModels={false}
/>
</FormShell>,
);
fireEvent.click(
screen.getByRole("button", { name: "providerForm.modelPicker.title" }),
);
const dialog = screen.getByRole("dialog");
// 带 [1m] 的行按去掉标记后的 ID 认成已勾选
expect(
within(dialog).getByRole("checkbox", { name: "glm-5.2" }),
).toBeChecked();
fireEvent.click(within(dialog).getByRole("checkbox", { name: "kimi-k3" }));
fireEvent.click(
within(dialog).getByRole("checkbox", { name: "minimax-m3" }),
);
fireEvent.click(
within(dialog).getByRole("button", {
name: "providerForm.modelPicker.apply",
}),
);
expect(onRowsChange).toHaveBeenCalledTimes(1);
const [rows] = onRowsChange.mock.calls[0];
expect(rows.slice(0, 2)).toEqual([first, draft]);
expect(rows).toHaveLength(3);
expect(rows[2]).toMatchObject({ model: "minimax-m3" });
});
});