mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-07-24 12:44:18 +08:00
feat(providers): add preset search and sorting (#3975)
This commit is contained in:
@@ -1,7 +1,21 @@
|
||||
import { useMemo, useState, type ReactNode } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FormLabel } from "@/components/ui/form";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { ClaudeIcon, CodexIcon, GeminiIcon } from "@/components/BrandIcons";
|
||||
import { Zap, Star, Layers, Settings2 } from "lucide-react";
|
||||
import { ArrowUpAZ, Search, Zap, Star, Layers, Settings2 } from "lucide-react";
|
||||
import type { ProviderPreset } from "@/config/claudeProviderPresets";
|
||||
import type { CodexProviderPreset } from "@/config/codexProviderPresets";
|
||||
import type { GeminiProviderPreset } from "@/config/geminiProviderPresets";
|
||||
@@ -16,7 +30,17 @@ import {
|
||||
} from "@/config/universalProviderPresets";
|
||||
import { ProviderIcon } from "@/components/ProviderIcon";
|
||||
|
||||
type AnyPreset =
|
||||
type PresetTranslator = (key: string) => unknown;
|
||||
|
||||
export const PresetSortMode = {
|
||||
Original: "original",
|
||||
NameAsc: "nameAsc",
|
||||
} as const;
|
||||
|
||||
export type PresetSortMode =
|
||||
(typeof PresetSortMode)[keyof typeof PresetSortMode];
|
||||
|
||||
export type AnyPreset =
|
||||
| ProviderPreset
|
||||
| CodexProviderPreset
|
||||
| GeminiProviderPreset
|
||||
@@ -25,11 +49,91 @@ type AnyPreset =
|
||||
| OpenClawProviderPreset
|
||||
| HermesProviderPreset;
|
||||
|
||||
type PresetEntry = {
|
||||
export type PresetEntry = {
|
||||
id: string;
|
||||
preset: AnyPreset;
|
||||
};
|
||||
|
||||
export function getPresetDisplayName(
|
||||
preset: AnyPreset,
|
||||
t: PresetTranslator,
|
||||
): string {
|
||||
return preset.nameKey ? String(t(preset.nameKey)) : preset.name;
|
||||
}
|
||||
|
||||
export function getPresetSearchText(
|
||||
entry: PresetEntry,
|
||||
presetCategoryLabels: Record<string, string>,
|
||||
t: PresetTranslator,
|
||||
): string {
|
||||
const presetCategory = entry.preset.category ?? "others";
|
||||
const categoryLabel =
|
||||
presetCategoryLabels[presetCategory] ?? String(t("providerPreset.other"));
|
||||
|
||||
return [
|
||||
getPresetDisplayName(entry.preset, t),
|
||||
entry.preset.name,
|
||||
entry.preset.websiteUrl,
|
||||
categoryLabel,
|
||||
]
|
||||
.join(" ")
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
export function filterPresetEntries(
|
||||
entries: PresetEntry[],
|
||||
query: string,
|
||||
presetCategoryLabels: Record<string, string>,
|
||||
t: PresetTranslator,
|
||||
): PresetEntry[] {
|
||||
const normalizedQuery = query.trim().toLowerCase();
|
||||
if (!normalizedQuery) {
|
||||
return entries;
|
||||
}
|
||||
|
||||
return entries.filter((entry) =>
|
||||
getPresetSearchText(entry, presetCategoryLabels, t).includes(
|
||||
normalizedQuery,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export function sortPresetEntries(
|
||||
entries: PresetEntry[],
|
||||
sortMode: PresetSortMode,
|
||||
t: PresetTranslator,
|
||||
): PresetEntry[] {
|
||||
if (sortMode === PresetSortMode.Original) {
|
||||
return [...entries];
|
||||
}
|
||||
|
||||
return [...entries].sort((a, b) =>
|
||||
getPresetDisplayName(a.preset, t).localeCompare(
|
||||
getPresetDisplayName(b.preset, t),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
export interface PresetVisibilityOptions {
|
||||
query: string;
|
||||
sortMode: PresetSortMode;
|
||||
presetCategoryLabels: Record<string, string>;
|
||||
t: PresetTranslator;
|
||||
}
|
||||
|
||||
export function getVisiblePresetEntries(
|
||||
entries: PresetEntry[],
|
||||
options: PresetVisibilityOptions,
|
||||
): PresetEntry[] {
|
||||
const { query, sortMode, presetCategoryLabels, t } = options;
|
||||
|
||||
return sortPresetEntries(
|
||||
filterPresetEntries(entries, query, presetCategoryLabels, t),
|
||||
sortMode,
|
||||
t,
|
||||
);
|
||||
}
|
||||
|
||||
interface ProviderPresetSelectorProps {
|
||||
selectedPresetId: string | null;
|
||||
presetEntries: PresetEntry[];
|
||||
@@ -50,8 +154,24 @@ export function ProviderPresetSelector({
|
||||
category,
|
||||
}: ProviderPresetSelectorProps) {
|
||||
const { t } = useTranslation();
|
||||
const [searchOpen, setSearchOpen] = useState(false);
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [sortMode, setSortMode] = useState<PresetSortMode>(
|
||||
PresetSortMode.Original,
|
||||
);
|
||||
|
||||
const getCategoryHint = (): React.ReactNode => {
|
||||
const visiblePresetEntries = useMemo(
|
||||
() =>
|
||||
getVisiblePresetEntries(presetEntries, {
|
||||
query: searchQuery,
|
||||
sortMode,
|
||||
presetCategoryLabels,
|
||||
t,
|
||||
}),
|
||||
[presetEntries, presetCategoryLabels, searchQuery, sortMode, t],
|
||||
);
|
||||
|
||||
const getCategoryHint = (): ReactNode => {
|
||||
switch (category) {
|
||||
case "official":
|
||||
return t("providerForm.officialHint", {
|
||||
@@ -85,6 +205,14 @@ export function ProviderPresetSelector({
|
||||
}
|
||||
};
|
||||
|
||||
const toggleSortMode = () => {
|
||||
setSortMode((current) =>
|
||||
current === PresetSortMode.Original
|
||||
? PresetSortMode.NameAsc
|
||||
: PresetSortMode.Original,
|
||||
);
|
||||
};
|
||||
|
||||
const renderPresetIcon = (preset: AnyPreset) => {
|
||||
const iconType = preset.theme?.icon;
|
||||
if (!iconType) return null;
|
||||
@@ -130,7 +258,88 @@ export function ProviderPresetSelector({
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<FormLabel>{t("providerPreset.label")}</FormLabel>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<FormLabel>{t("providerPreset.label")}</FormLabel>
|
||||
<TooltipProvider delayDuration={300}>
|
||||
<div className="flex items-center gap-1">
|
||||
<Popover open={searchOpen} onOpenChange={setSearchOpen}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
aria-label={t("providerPreset.searchAriaLabel", {
|
||||
defaultValue: "Search provider presets",
|
||||
})}
|
||||
className={
|
||||
searchQuery.trim()
|
||||
? "size-8 bg-accent text-foreground"
|
||||
: "size-8"
|
||||
}
|
||||
>
|
||||
<Search className="size-4" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{t("providerPreset.searchTooltip", {
|
||||
defaultValue: "Search presets",
|
||||
})}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<PopoverContent
|
||||
align="end"
|
||||
className="w-72 p-2 border-border-default"
|
||||
>
|
||||
<Input
|
||||
value={searchQuery}
|
||||
onChange={(event) => setSearchQuery(event.target.value)}
|
||||
placeholder={t("providerPreset.searchPlaceholder", {
|
||||
defaultValue: "Search presets...",
|
||||
})}
|
||||
aria-label={t("providerPreset.searchAriaLabel", {
|
||||
defaultValue: "Search provider presets",
|
||||
})}
|
||||
autoFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
aria-label={t("providerPreset.sortAriaLabel", {
|
||||
defaultValue: "Toggle preset sorting",
|
||||
})}
|
||||
aria-pressed={sortMode === PresetSortMode.NameAsc}
|
||||
onClick={toggleSortMode}
|
||||
className={
|
||||
sortMode === PresetSortMode.NameAsc
|
||||
? "size-8 bg-accent text-foreground"
|
||||
: "size-8"
|
||||
}
|
||||
>
|
||||
<ArrowUpAZ className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{sortMode === PresetSortMode.NameAsc
|
||||
? t("providerPreset.sortOriginalTooltip", {
|
||||
defaultValue: "Restore original order",
|
||||
})
|
||||
: t("providerPreset.sortNameAscTooltip", {
|
||||
defaultValue: "Sort A-Z",
|
||||
})}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
type="button"
|
||||
@@ -144,7 +353,15 @@ export function ProviderPresetSelector({
|
||||
{t("providerPreset.custom")}
|
||||
</button>
|
||||
|
||||
{presetEntries.map((entry) => {
|
||||
{visiblePresetEntries.length === 0 && (
|
||||
<div className="w-full rounded-md border border-dashed border-border-default px-3 py-2 text-xs text-muted-foreground">
|
||||
{t("providerPreset.noSearchResults", {
|
||||
defaultValue: "No matching presets.",
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{visiblePresetEntries.map((entry) => {
|
||||
const isSelected = selectedPresetId === entry.id;
|
||||
const isPartner = entry.preset.isPartner;
|
||||
const presetCategory = entry.preset.category ?? "others";
|
||||
@@ -161,9 +378,7 @@ export function ProviderPresetSelector({
|
||||
}
|
||||
>
|
||||
{renderPresetIcon(entry.preset)}
|
||||
{entry.preset.nameKey
|
||||
? t(entry.preset.nameKey)
|
||||
: entry.preset.name}
|
||||
{getPresetDisplayName(entry.preset, t)}
|
||||
{isPartner && (
|
||||
<span className="absolute -top-1 -right-1 flex items-center gap-0.5 rounded-full bg-gradient-to-r from-amber-500 to-yellow-500 px-1.5 py-0.5 text-[10px] font-bold text-white shadow-md">
|
||||
<Star className="h-2.5 w-2.5 fill-current" />
|
||||
|
||||
@@ -1327,7 +1327,14 @@
|
||||
"label": "Provider Preset",
|
||||
"custom": "Custom Configuration",
|
||||
"other": "Other",
|
||||
"hint": "You can continue to adjust the fields below after selecting a preset."
|
||||
"hint": "You can continue to adjust the fields below after selecting a preset.",
|
||||
"searchTooltip": "Search presets",
|
||||
"searchAriaLabel": "Search provider presets",
|
||||
"searchPlaceholder": "Search name, website, or category...",
|
||||
"sortAriaLabel": "Toggle preset sorting",
|
||||
"sortNameAscTooltip": "Sort by name A-Z",
|
||||
"sortOriginalTooltip": "Restore original order",
|
||||
"noSearchResults": "No provider presets match your search."
|
||||
},
|
||||
"usage": {
|
||||
"title": "Usage Statistics",
|
||||
|
||||
@@ -1327,7 +1327,14 @@
|
||||
"label": "プロバイダータイプ",
|
||||
"custom": "カスタム設定",
|
||||
"other": "その他",
|
||||
"hint": "プリセットを選んだ後でも、下のフィールドで調整できます。"
|
||||
"hint": "プリセットを選んだ後でも、下のフィールドで調整できます。",
|
||||
"searchTooltip": "プリセットを検索",
|
||||
"searchAriaLabel": "プロバイダープリセットを検索",
|
||||
"searchPlaceholder": "名前、サイト、カテゴリで検索...",
|
||||
"sortAriaLabel": "プリセットの並び順を切り替え",
|
||||
"sortNameAscTooltip": "名前順 A-Z で並び替え",
|
||||
"sortOriginalTooltip": "元の順序に戻す",
|
||||
"noSearchResults": "一致するプロバイダープリセットがありません。"
|
||||
},
|
||||
"usage": {
|
||||
"title": "利用統計",
|
||||
|
||||
@@ -1299,7 +1299,14 @@
|
||||
"label": "預設供應商",
|
||||
"custom": "自訂設定",
|
||||
"other": "其他",
|
||||
"hint": "選擇預設後可繼續調整下方欄位。"
|
||||
"hint": "選擇預設後可繼續調整下方欄位。",
|
||||
"searchTooltip": "搜尋預設",
|
||||
"searchAriaLabel": "搜尋預設供應商",
|
||||
"searchPlaceholder": "搜尋名稱、官網或分類...",
|
||||
"sortAriaLabel": "切換預設排序",
|
||||
"sortNameAscTooltip": "按名稱 A-Z 排序",
|
||||
"sortOriginalTooltip": "恢復原始順序",
|
||||
"noSearchResults": "沒有相符的預設供應商。"
|
||||
},
|
||||
"usage": {
|
||||
"title": "使用統計",
|
||||
|
||||
@@ -1327,7 +1327,14 @@
|
||||
"label": "预设供应商",
|
||||
"custom": "自定义配置",
|
||||
"other": "其他",
|
||||
"hint": "选择预设后可继续调整下方字段。"
|
||||
"hint": "选择预设后可继续调整下方字段。",
|
||||
"searchTooltip": "搜索预设",
|
||||
"searchAriaLabel": "搜索预设供应商",
|
||||
"searchPlaceholder": "搜索名称、官网或分类...",
|
||||
"sortAriaLabel": "切换预设排序",
|
||||
"sortNameAscTooltip": "按名称 A-Z 排序",
|
||||
"sortOriginalTooltip": "恢复原始顺序",
|
||||
"noSearchResults": "没有匹配的预设供应商。"
|
||||
},
|
||||
"usage": {
|
||||
"title": "使用统计",
|
||||
|
||||
@@ -1,71 +1,323 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { TFunction } from "i18next";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import { ProviderPresetSelector } from "@/components/providers/forms/ProviderPresetSelector";
|
||||
import type { ProviderCategory } from "@/types";
|
||||
import {
|
||||
ProviderPresetSelector,
|
||||
filterPresetEntries,
|
||||
getPresetDisplayName,
|
||||
getPresetSearchText,
|
||||
getVisiblePresetEntries,
|
||||
sortPresetEntries,
|
||||
type PresetSortMode,
|
||||
} from "@/components/providers/forms/ProviderPresetSelector";
|
||||
|
||||
describe("ProviderPresetSelector", () => {
|
||||
it("按传入的预设数组顺序渲染,不按分类重新排序", () => {
|
||||
const Wrapper = () => {
|
||||
const form = useForm();
|
||||
const presetCategoryLabels = {
|
||||
official: "官方",
|
||||
cn_official: "国产官方",
|
||||
aggregator: "聚合服务",
|
||||
third_party: "第三方",
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<ProviderPresetSelector
|
||||
selectedPresetId="custom"
|
||||
presetEntries={[
|
||||
{
|
||||
id: "preset-0",
|
||||
preset: {
|
||||
name: "First",
|
||||
websiteUrl: "https://first.example.com",
|
||||
settingsConfig: {},
|
||||
category: "third_party",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "preset-1",
|
||||
preset: {
|
||||
name: "Second",
|
||||
websiteUrl: "https://second.example.com",
|
||||
settingsConfig: {},
|
||||
category: "official",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "preset-2",
|
||||
preset: {
|
||||
name: "Third",
|
||||
websiteUrl: "https://third.example.com",
|
||||
settingsConfig: {},
|
||||
category: "aggregator",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "preset-3",
|
||||
preset: {
|
||||
name: "Fourth",
|
||||
websiteUrl: "https://fourth.example.com",
|
||||
settingsConfig: {},
|
||||
category: "official",
|
||||
},
|
||||
},
|
||||
]}
|
||||
presetCategoryLabels={{
|
||||
official: "官方",
|
||||
aggregator: "聚合服务",
|
||||
third_party: "第三方",
|
||||
}}
|
||||
onPresetChange={vi.fn()}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
const translations: Record<string, string> = {
|
||||
"preset.alpha": "Alpha 本地名",
|
||||
"preset.gamma": "Gamma 本地名",
|
||||
};
|
||||
|
||||
render(<Wrapper />);
|
||||
const t = ((key: string) => translations[key] ?? key) as TFunction;
|
||||
|
||||
type TestPresetEntry = {
|
||||
id: string;
|
||||
preset: {
|
||||
name: string;
|
||||
nameKey?: string;
|
||||
websiteUrl: string;
|
||||
settingsConfig: Record<string, never>;
|
||||
category: ProviderCategory;
|
||||
};
|
||||
};
|
||||
|
||||
const presetEntries: TestPresetEntry[] = [
|
||||
{
|
||||
id: "gamma",
|
||||
preset: {
|
||||
name: "Gamma Raw",
|
||||
nameKey: "preset.gamma",
|
||||
websiteUrl: "https://gamma.example.com",
|
||||
settingsConfig: {},
|
||||
category: "aggregator",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "alpha",
|
||||
preset: {
|
||||
name: "Alpha Raw",
|
||||
nameKey: "preset.alpha",
|
||||
websiteUrl: "https://alpha.example.com/v1",
|
||||
settingsConfig: {},
|
||||
category: "official",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "beta",
|
||||
preset: {
|
||||
name: "Beta Gateway",
|
||||
websiteUrl: "https://CN-Gateway.example.com",
|
||||
settingsConfig: {},
|
||||
category: "cn_official",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "delta",
|
||||
preset: {
|
||||
name: "Delta Mirror",
|
||||
websiteUrl: "https://delta.example.com",
|
||||
settingsConfig: {},
|
||||
category: "third_party",
|
||||
},
|
||||
},
|
||||
] satisfies TestPresetEntry[];
|
||||
|
||||
function getIds(entries: ReadonlyArray<{ id: string }>) {
|
||||
return entries.map((entry) => entry.id);
|
||||
}
|
||||
|
||||
function renderSelector({
|
||||
entries = presetEntries,
|
||||
onPresetChange = vi.fn(),
|
||||
}: {
|
||||
entries?: TestPresetEntry[];
|
||||
onPresetChange?: (value: string) => void;
|
||||
} = {}) {
|
||||
const Wrapper = () => {
|
||||
const form = useForm();
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<ProviderPresetSelector
|
||||
selectedPresetId="custom"
|
||||
presetEntries={entries}
|
||||
presetCategoryLabels={presetCategoryLabels}
|
||||
onPresetChange={onPresetChange}
|
||||
/>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
return render(<Wrapper />);
|
||||
}
|
||||
|
||||
function getPresetButtonTexts() {
|
||||
const knownNames = new Set([
|
||||
"providerPreset.custom",
|
||||
...presetEntries.flatMap((entry) => [
|
||||
entry.preset.name,
|
||||
entry.preset.nameKey ?? entry.preset.name,
|
||||
]),
|
||||
]);
|
||||
|
||||
return screen
|
||||
.getAllByRole("button")
|
||||
.map((button) => button.textContent?.trim() ?? "")
|
||||
.filter((text) => knownNames.has(text));
|
||||
}
|
||||
|
||||
function getSearchButton() {
|
||||
return screen.getByRole("button", {
|
||||
name: /providerPreset\.(search|searchAriaLabel|openSearch)|搜索|search/i,
|
||||
});
|
||||
}
|
||||
|
||||
function getSortButton() {
|
||||
return screen.getByRole("button", {
|
||||
name: /providerPreset\.(sort|sortByName|restoreOriginalOrder)|按名称排序|恢复原顺序|sort/i,
|
||||
});
|
||||
}
|
||||
|
||||
function getSearchInput() {
|
||||
return screen.getByRole("textbox", {
|
||||
name: /providerPreset\.(searchInput|searchPlaceholder)|搜索预设|search/i,
|
||||
});
|
||||
}
|
||||
|
||||
describe("ProviderPresetSelector pure helpers", () => {
|
||||
it("优先使用 nameKey 翻译作为显示名,否则使用原始 name", () => {
|
||||
expect(getPresetDisplayName(presetEntries[1].preset, t)).toBe(
|
||||
"Alpha 本地名",
|
||||
);
|
||||
expect(getPresetDisplayName(presetEntries[2].preset, t)).toBe(
|
||||
"Beta Gateway",
|
||||
);
|
||||
});
|
||||
|
||||
it("拼接显示名、原始名称、URL、分类 label,并统一 lower-case", () => {
|
||||
const searchText = getPresetSearchText(
|
||||
presetEntries[1],
|
||||
presetCategoryLabels,
|
||||
t,
|
||||
);
|
||||
|
||||
expect(searchText).toContain("alpha 本地名");
|
||||
expect(searchText).toContain("alpha raw");
|
||||
expect(searchText).toContain("https://alpha.example.com/v1");
|
||||
expect(searchText).toContain("官方");
|
||||
expect(searchText).toBe(searchText.toLowerCase());
|
||||
});
|
||||
|
||||
it("空 query 返回原数组,非空 query 大小写不敏感匹配", () => {
|
||||
expect(
|
||||
filterPresetEntries(presetEntries, " ", presetCategoryLabels, t),
|
||||
).toBe(presetEntries);
|
||||
expect(
|
||||
getIds(
|
||||
filterPresetEntries(
|
||||
presetEntries,
|
||||
"ALPHA 本地名",
|
||||
presetCategoryLabels,
|
||||
t,
|
||||
),
|
||||
),
|
||||
).toEqual(["alpha"]);
|
||||
});
|
||||
|
||||
it("支持通过 URL 和分类 label 搜索", () => {
|
||||
expect(
|
||||
getIds(
|
||||
filterPresetEntries(
|
||||
presetEntries,
|
||||
"cn-gateway.example.com",
|
||||
presetCategoryLabels,
|
||||
t,
|
||||
),
|
||||
),
|
||||
).toEqual(["beta"]);
|
||||
expect(
|
||||
getIds(
|
||||
filterPresetEntries(presetEntries, "聚合", presetCategoryLabels, t),
|
||||
),
|
||||
).toEqual(["gamma"]);
|
||||
});
|
||||
|
||||
it("支持 A-Z 排序、original 副本恢复原顺序,并且 getVisible 先 filter 再 sort", () => {
|
||||
const originalMode: PresetSortMode = "original";
|
||||
const nameAscMode: PresetSortMode = "nameAsc";
|
||||
|
||||
const original = sortPresetEntries(presetEntries, originalMode, t);
|
||||
expect(original).not.toBe(presetEntries);
|
||||
expect(getIds(original)).toEqual(["gamma", "alpha", "beta", "delta"]);
|
||||
|
||||
expect(getIds(sortPresetEntries(presetEntries, nameAscMode, t))).toEqual([
|
||||
"alpha",
|
||||
"beta",
|
||||
"delta",
|
||||
"gamma",
|
||||
]);
|
||||
expect(getIds(presetEntries)).toEqual(["gamma", "alpha", "beta", "delta"]);
|
||||
|
||||
expect(
|
||||
screen.getAllByRole("button").map((button) => button.textContent),
|
||||
).toEqual(["providerPreset.custom", "First", "Second", "Third", "Fourth"]);
|
||||
getIds(
|
||||
getVisiblePresetEntries(presetEntries, {
|
||||
query: "a",
|
||||
sortMode: nameAscMode,
|
||||
presetCategoryLabels,
|
||||
t,
|
||||
}),
|
||||
),
|
||||
).toEqual(["alpha", "beta", "delta", "gamma"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ProviderPresetSelector", () => {
|
||||
it("默认按传入的预设数组顺序渲染,不按分类或名称重新排序", () => {
|
||||
renderSelector();
|
||||
|
||||
expect(getPresetButtonTexts()).toEqual([
|
||||
"providerPreset.custom",
|
||||
"preset.gamma",
|
||||
"preset.alpha",
|
||||
"Beta Gateway",
|
||||
"Delta Mirror",
|
||||
]);
|
||||
});
|
||||
|
||||
it("点击排序按钮后普通 preset A-Z,再点恢复原顺序", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderSelector();
|
||||
|
||||
await user.click(getSortButton());
|
||||
|
||||
expect(getPresetButtonTexts()).toEqual([
|
||||
"providerPreset.custom",
|
||||
"Beta Gateway",
|
||||
"Delta Mirror",
|
||||
"preset.alpha",
|
||||
"preset.gamma",
|
||||
]);
|
||||
|
||||
await user.click(getSortButton());
|
||||
|
||||
expect(getPresetButtonTexts()).toEqual([
|
||||
"providerPreset.custom",
|
||||
"preset.gamma",
|
||||
"preset.alpha",
|
||||
"Beta Gateway",
|
||||
"Delta Mirror",
|
||||
]);
|
||||
});
|
||||
|
||||
it("搜索只过滤普通 preset,自定义配置始终保留", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderSelector();
|
||||
|
||||
await user.click(getSearchButton());
|
||||
await user.type(getSearchInput(), "gateway");
|
||||
|
||||
expect(
|
||||
screen.getByRole("button", { name: "providerPreset.custom" }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByRole("button", { name: "Beta Gateway" }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole("button", { name: "preset.gamma" }),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole("button", { name: "preset.alpha" }),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole("button", { name: "Delta Mirror" }),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("搜索无普通 preset 结果时保留自定义配置并显示空状态", async () => {
|
||||
const user = userEvent.setup();
|
||||
renderSelector();
|
||||
|
||||
await user.click(getSearchButton());
|
||||
await user.type(getSearchInput(), "not-found");
|
||||
|
||||
expect(
|
||||
screen.getByRole("button", { name: "providerPreset.custom" }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole("button", { name: "preset.gamma" }),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole("button", { name: "preset.alpha" }),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole("button", { name: "Beta Gateway" }),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByRole("button", { name: "Delta Mirror" }),
|
||||
).not.toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
/providerPreset\.(empty|noResults)|没有匹配|无结果|no matching presets/i,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user