adaptive-thinking-editor.test.tsx 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. /**
  2. * @vitest-environment happy-dom
  3. */
  4. import { act } from "react";
  5. import { createRoot } from "react-dom/client";
  6. import { describe, expect, it, vi } from "vitest";
  7. import { AdaptiveThinkingEditor } from "@/app/[locale]/settings/providers/_components/adaptive-thinking-editor";
  8. import type { AnthropicAdaptiveThinkingConfig } from "@/types/provider";
  9. // Mock next-intl
  10. vi.mock("next-intl", () => ({
  11. useTranslations: () => (key: string) => key,
  12. }));
  13. // Mock UI components
  14. vi.mock("@/components/ui/select", () => ({
  15. Select: ({
  16. children,
  17. value,
  18. onValueChange,
  19. disabled,
  20. }: {
  21. children: React.ReactNode;
  22. value: string;
  23. onValueChange: (val: string) => void;
  24. disabled?: boolean;
  25. }) => (
  26. <div data-testid="select-mock">
  27. <select
  28. data-testid="select-trigger"
  29. value={value}
  30. onChange={(e) => onValueChange(e.target.value)}
  31. disabled={disabled}
  32. >
  33. {children}
  34. </select>
  35. </div>
  36. ),
  37. SelectTrigger: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
  38. SelectValue: () => null,
  39. SelectContent: ({ children }: { children: React.ReactNode }) => <>{children}</>,
  40. SelectItem: ({ value, children }: { value: string; children: React.ReactNode }) => (
  41. <option value={value}>{children}</option>
  42. ),
  43. }));
  44. vi.mock("@/components/ui/switch", () => ({
  45. Switch: ({
  46. checked,
  47. onCheckedChange,
  48. disabled,
  49. }: {
  50. checked: boolean;
  51. onCheckedChange: (checked: boolean) => void;
  52. disabled?: boolean;
  53. }) => (
  54. <button
  55. data-testid="switch"
  56. onClick={() => onCheckedChange(!checked)}
  57. disabled={disabled}
  58. aria-checked={checked}
  59. >
  60. {checked ? "On" : "Off"}
  61. </button>
  62. ),
  63. }));
  64. vi.mock("@/components/ui/tag-input", () => ({
  65. TagInput: ({
  66. value,
  67. onChange,
  68. disabled,
  69. placeholder,
  70. }: {
  71. value: string[];
  72. onChange: (tags: string[]) => void;
  73. disabled?: boolean;
  74. placeholder?: string;
  75. }) => (
  76. <input
  77. data-testid="tag-input"
  78. value={value.join(",")}
  79. onChange={(e) => onChange(e.target.value.split(",").filter(Boolean))}
  80. disabled={disabled}
  81. placeholder={placeholder}
  82. />
  83. ),
  84. }));
  85. vi.mock("@/components/ui/tooltip", () => ({
  86. Tooltip: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
  87. TooltipTrigger: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
  88. TooltipContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
  89. }));
  90. vi.mock("./forms/provider-form/components/section-card", () => ({
  91. SmartInputWrapper: ({ label, children }: { label: string; children: React.ReactNode }) => (
  92. <div data-testid="smart-input-wrapper">
  93. <label>{label}</label>
  94. {children}
  95. </div>
  96. ),
  97. ToggleRow: ({ label, children }: { label: string; children: React.ReactNode }) => (
  98. <div data-testid="toggle-row">
  99. <label>{label}</label>
  100. {children}
  101. </div>
  102. ),
  103. }));
  104. // Mock lucide-react
  105. vi.mock("lucide-react", () => ({
  106. Info: () => <div data-testid="info-icon" />,
  107. }));
  108. function render(node: React.ReactNode) {
  109. const container = document.createElement("div");
  110. document.body.appendChild(container);
  111. const root = createRoot(container);
  112. act(() => {
  113. root.render(node);
  114. });
  115. return {
  116. container,
  117. unmount: () => {
  118. act(() => root.unmount());
  119. container.remove();
  120. },
  121. };
  122. }
  123. describe("AdaptiveThinkingEditor", () => {
  124. const defaultConfig: AnthropicAdaptiveThinkingConfig = {
  125. effort: "medium",
  126. modelMatchMode: "all",
  127. models: [],
  128. };
  129. const mockOnEnabledChange = vi.fn();
  130. const mockOnConfigChange = vi.fn();
  131. it("renders correctly in disabled state (switch off)", () => {
  132. const { container, unmount } = render(
  133. <AdaptiveThinkingEditor
  134. enabled={false}
  135. config={defaultConfig}
  136. onEnabledChange={mockOnEnabledChange}
  137. onConfigChange={mockOnConfigChange}
  138. />
  139. );
  140. const switchBtn = container.querySelector('[data-testid="switch"]');
  141. expect(switchBtn).toBeTruthy();
  142. expect(switchBtn?.textContent).toBe("Off");
  143. expect(container.querySelector('[data-testid="select-trigger"]')).toBeNull();
  144. unmount();
  145. });
  146. it("calls onEnabledChange when switch is clicked", () => {
  147. const { container, unmount } = render(
  148. <AdaptiveThinkingEditor
  149. enabled={false}
  150. config={defaultConfig}
  151. onEnabledChange={mockOnEnabledChange}
  152. onConfigChange={mockOnConfigChange}
  153. />
  154. );
  155. const switchBtn = container.querySelector('[data-testid="switch"]') as HTMLButtonElement;
  156. act(() => {
  157. switchBtn.click();
  158. });
  159. expect(mockOnEnabledChange).toHaveBeenCalledWith(true);
  160. unmount();
  161. });
  162. it("renders configuration options when enabled", () => {
  163. const { container, unmount } = render(
  164. <AdaptiveThinkingEditor
  165. enabled={true}
  166. config={defaultConfig}
  167. onEnabledChange={mockOnEnabledChange}
  168. onConfigChange={mockOnConfigChange}
  169. />
  170. );
  171. const switchBtn = container.querySelector('[data-testid="switch"]');
  172. expect(switchBtn?.textContent).toBe("On");
  173. // Should have 2 selects: effort and mode (since mode is 'all')
  174. const selects = container.querySelectorAll('[data-testid="select-trigger"]');
  175. expect(selects.length).toBe(2);
  176. unmount();
  177. });
  178. it("calls onConfigChange when effort is changed", () => {
  179. const { container, unmount } = render(
  180. <AdaptiveThinkingEditor
  181. enabled={true}
  182. config={defaultConfig}
  183. onEnabledChange={mockOnEnabledChange}
  184. onConfigChange={mockOnConfigChange}
  185. />
  186. );
  187. const selects = container.querySelectorAll("select");
  188. // First select is effort
  189. const effortSelect = selects[0];
  190. act(() => {
  191. effortSelect.value = "high";
  192. effortSelect.dispatchEvent(new Event("change", { bubbles: true }));
  193. });
  194. expect(mockOnConfigChange).toHaveBeenCalledWith({
  195. ...defaultConfig,
  196. effort: "high",
  197. });
  198. unmount();
  199. });
  200. it("calls onConfigChange when model match mode is changed", () => {
  201. const { container, unmount } = render(
  202. <AdaptiveThinkingEditor
  203. enabled={true}
  204. config={defaultConfig}
  205. onEnabledChange={mockOnEnabledChange}
  206. onConfigChange={mockOnConfigChange}
  207. />
  208. );
  209. const selects = container.querySelectorAll("select");
  210. // Second select is model match mode
  211. const modeSelect = selects[1];
  212. act(() => {
  213. modeSelect.value = "specific";
  214. modeSelect.dispatchEvent(new Event("change", { bubbles: true }));
  215. });
  216. expect(mockOnConfigChange).toHaveBeenCalledWith({
  217. ...defaultConfig,
  218. modelMatchMode: "specific",
  219. });
  220. unmount();
  221. });
  222. it("renders model input when mode is specific", () => {
  223. const specificConfig: AnthropicAdaptiveThinkingConfig = {
  224. ...defaultConfig,
  225. modelMatchMode: "specific",
  226. };
  227. const { container, unmount } = render(
  228. <AdaptiveThinkingEditor
  229. enabled={true}
  230. config={specificConfig}
  231. onEnabledChange={mockOnEnabledChange}
  232. onConfigChange={mockOnConfigChange}
  233. />
  234. );
  235. expect(container.querySelector('[data-testid="tag-input"]')).toBeTruthy();
  236. unmount();
  237. });
  238. it("calls onConfigChange when models are changed", () => {
  239. const specificConfig: AnthropicAdaptiveThinkingConfig = {
  240. ...defaultConfig,
  241. modelMatchMode: "specific",
  242. };
  243. const { container, unmount } = render(
  244. <AdaptiveThinkingEditor
  245. enabled={true}
  246. config={specificConfig}
  247. onEnabledChange={mockOnEnabledChange}
  248. onConfigChange={mockOnConfigChange}
  249. />
  250. );
  251. const input = container.querySelector('[data-testid="tag-input"]') as HTMLInputElement;
  252. act(() => {
  253. // Simulate typing a tag
  254. // For standard HTML inputs, simply setting value and dispatching event works
  255. // The Object.getOwnPropertyDescriptor trick is needed for React controlled inputs
  256. // but here we are using a mocked input which might just need the event
  257. const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
  258. window.HTMLInputElement.prototype,
  259. "value"
  260. )?.set;
  261. nativeInputValueSetter?.call(input, "claude-3-5-sonnet");
  262. input.dispatchEvent(new Event("change", { bubbles: true }));
  263. });
  264. expect(mockOnConfigChange).toHaveBeenCalledWith({
  265. ...specificConfig,
  266. models: ["claude-3-5-sonnet"],
  267. });
  268. unmount();
  269. });
  270. it("passes disabled prop to children", () => {
  271. const { container, unmount } = render(
  272. <AdaptiveThinkingEditor
  273. enabled={true}
  274. config={defaultConfig}
  275. onEnabledChange={mockOnEnabledChange}
  276. onConfigChange={mockOnConfigChange}
  277. disabled={true}
  278. />
  279. );
  280. const switchBtn = container.querySelector('[data-testid="switch"]') as HTMLButtonElement;
  281. expect(switchBtn.disabled).toBe(true);
  282. const selects = container.querySelectorAll("select");
  283. selects.forEach((select) => {
  284. expect(select.disabled).toBe(true);
  285. });
  286. unmount();
  287. });
  288. });