thinking-budget-editor.test.tsx 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  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, beforeEach } from "vitest";
  7. import { ThinkingBudgetEditor } from "@/app/[locale]/settings/providers/_components/thinking-budget-editor";
  8. // Mock next-intl
  9. vi.mock("next-intl", () => ({
  10. useTranslations: () => (key: string) => key,
  11. }));
  12. // Mock Select as native <select> (same pattern as adaptive-thinking-editor)
  13. vi.mock("@/components/ui/select", () => ({
  14. Select: ({
  15. children,
  16. value,
  17. onValueChange,
  18. disabled,
  19. }: {
  20. children: React.ReactNode;
  21. value: string;
  22. onValueChange: (val: string) => void;
  23. disabled?: boolean;
  24. }) => (
  25. <div data-testid="select-mock">
  26. <select
  27. data-testid="select-trigger"
  28. value={value}
  29. onChange={(e) => onValueChange(e.target.value)}
  30. disabled={disabled}
  31. >
  32. {children}
  33. </select>
  34. </div>
  35. ),
  36. SelectTrigger: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
  37. SelectValue: () => null,
  38. SelectContent: ({ children }: { children: React.ReactNode }) => <>{children}</>,
  39. SelectItem: ({ value, children }: { value: string; children: React.ReactNode }) => (
  40. <option value={value}>{children}</option>
  41. ),
  42. }));
  43. // Mock Tooltip as passthrough
  44. vi.mock("@/components/ui/tooltip", () => ({
  45. Tooltip: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
  46. TooltipTrigger: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
  47. TooltipContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
  48. }));
  49. // Mock lucide-react
  50. vi.mock("lucide-react", () => ({
  51. Info: () => <div data-testid="info-icon" />,
  52. }));
  53. function render(node: React.ReactNode) {
  54. const container = document.createElement("div");
  55. document.body.appendChild(container);
  56. const root = createRoot(container);
  57. act(() => {
  58. root.render(node);
  59. });
  60. return {
  61. container,
  62. unmount: () => {
  63. act(() => root.unmount());
  64. container.remove();
  65. },
  66. };
  67. }
  68. describe("ThinkingBudgetEditor", () => {
  69. const defaultProps = {
  70. value: "inherit",
  71. onChange: vi.fn(),
  72. disabled: false,
  73. };
  74. beforeEach(() => {
  75. vi.clearAllMocks();
  76. });
  77. it("renders with inherit value - no numeric input or max button", () => {
  78. const { container, unmount } = render(<ThinkingBudgetEditor {...defaultProps} />);
  79. const select = container.querySelector('[data-testid="select-trigger"]') as HTMLSelectElement;
  80. expect(select).toBeTruthy();
  81. expect(select.value).toBe("inherit");
  82. // No number input when inherit
  83. expect(container.querySelector('input[type="number"]')).toBeNull();
  84. // No max-out button when inherit
  85. expect(container.querySelector("button")).toBeNull();
  86. unmount();
  87. });
  88. it("renders with numeric value - shows custom select, input, and max button", () => {
  89. const { container, unmount } = render(<ThinkingBudgetEditor {...defaultProps} value="15000" />);
  90. const select = container.querySelector('[data-testid="select-trigger"]') as HTMLSelectElement;
  91. expect(select.value).toBe("custom");
  92. const input = container.querySelector('input[type="number"]') as HTMLInputElement;
  93. expect(input).toBeTruthy();
  94. expect(input.value).toBe("15000");
  95. const maxButton = container.querySelector("button");
  96. expect(maxButton).toBeTruthy();
  97. expect(maxButton?.textContent).toContain("maxOutButton");
  98. unmount();
  99. });
  100. it("switches from inherit to custom - calls onChange with 10240", () => {
  101. const onChange = vi.fn();
  102. const { container, unmount } = render(
  103. <ThinkingBudgetEditor {...defaultProps} onChange={onChange} />
  104. );
  105. const select = container.querySelector('[data-testid="select-trigger"]') as HTMLSelectElement;
  106. act(() => {
  107. select.value = "custom";
  108. select.dispatchEvent(new Event("change", { bubbles: true }));
  109. });
  110. expect(onChange).toHaveBeenCalledWith("10240");
  111. unmount();
  112. });
  113. it("switches from custom to inherit - calls onChange with inherit", () => {
  114. const onChange = vi.fn();
  115. const { container, unmount } = render(
  116. <ThinkingBudgetEditor {...defaultProps} value="20000" onChange={onChange} />
  117. );
  118. const select = container.querySelector('[data-testid="select-trigger"]') as HTMLSelectElement;
  119. act(() => {
  120. select.value = "inherit";
  121. select.dispatchEvent(new Event("change", { bubbles: true }));
  122. });
  123. expect(onChange).toHaveBeenCalledWith("inherit");
  124. unmount();
  125. });
  126. it("clicking max-out button calls onChange with 32000", () => {
  127. const onChange = vi.fn();
  128. const { container, unmount } = render(
  129. <ThinkingBudgetEditor {...defaultProps} value="10000" onChange={onChange} />
  130. );
  131. const maxButton = container.querySelector("button") as HTMLButtonElement;
  132. act(() => {
  133. maxButton.click();
  134. });
  135. expect(onChange).toHaveBeenCalledWith("32000");
  136. unmount();
  137. });
  138. it("typing a number calls onChange with that value", () => {
  139. const onChange = vi.fn();
  140. const { container, unmount } = render(
  141. <ThinkingBudgetEditor {...defaultProps} value="10000" onChange={onChange} />
  142. );
  143. const input = container.querySelector('input[type="number"]') as HTMLInputElement;
  144. act(() => {
  145. const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
  146. window.HTMLInputElement.prototype,
  147. "value"
  148. )?.set;
  149. nativeInputValueSetter?.call(input, "12345");
  150. input.dispatchEvent(new Event("change", { bubbles: true }));
  151. });
  152. expect(onChange).toHaveBeenCalledWith("12345");
  153. unmount();
  154. });
  155. it("clearing input calls onChange with inherit", () => {
  156. const onChange = vi.fn();
  157. const { container, unmount } = render(
  158. <ThinkingBudgetEditor {...defaultProps} value="10000" onChange={onChange} />
  159. );
  160. const input = container.querySelector('input[type="number"]') as HTMLInputElement;
  161. act(() => {
  162. const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
  163. window.HTMLInputElement.prototype,
  164. "value"
  165. )?.set;
  166. nativeInputValueSetter?.call(input, "");
  167. input.dispatchEvent(new Event("change", { bubbles: true }));
  168. });
  169. expect(onChange).toHaveBeenCalledWith("");
  170. unmount();
  171. });
  172. it("disabled prop disables all controls", () => {
  173. const { container, unmount } = render(
  174. <ThinkingBudgetEditor {...defaultProps} disabled={true} value="10000" />
  175. );
  176. const select = container.querySelector('[data-testid="select-trigger"]') as HTMLSelectElement;
  177. expect(select.disabled).toBe(true);
  178. const input = container.querySelector('input[type="number"]') as HTMLInputElement;
  179. expect(input.disabled).toBe(true);
  180. const maxButton = container.querySelector("button") as HTMLButtonElement;
  181. expect(maxButton.disabled).toBe(true);
  182. unmount();
  183. });
  184. });