thinking-budget-editor.test.tsx 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  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 (help button always exists)
  85. const buttons = Array.from(container.querySelectorAll("button"));
  86. expect(buttons.some((b) => b.textContent?.includes("maxOutButton"))).toBe(false);
  87. // Help icon should exist
  88. expect(container.querySelector('[data-testid="info-icon"]')).toBeTruthy();
  89. unmount();
  90. });
  91. it("renders with numeric value - shows custom select, input, and max button", () => {
  92. const { container, unmount } = render(<ThinkingBudgetEditor {...defaultProps} value="15000" />);
  93. const select = container.querySelector('[data-testid="select-trigger"]') as HTMLSelectElement;
  94. expect(select.value).toBe("custom");
  95. const input = container.querySelector('input[type="number"]') as HTMLInputElement;
  96. expect(input).toBeTruthy();
  97. expect(input.value).toBe("15000");
  98. const maxButton = Array.from(container.querySelectorAll("button")).find((b) =>
  99. b.textContent?.includes("maxOutButton")
  100. );
  101. expect(maxButton).toBeTruthy();
  102. expect(maxButton?.textContent).toContain("maxOutButton");
  103. unmount();
  104. });
  105. it("switches from inherit to custom - calls onChange with 10240", () => {
  106. const onChange = vi.fn();
  107. const { container, unmount } = render(
  108. <ThinkingBudgetEditor {...defaultProps} onChange={onChange} />
  109. );
  110. const select = container.querySelector('[data-testid="select-trigger"]') as HTMLSelectElement;
  111. act(() => {
  112. select.value = "custom";
  113. select.dispatchEvent(new Event("change", { bubbles: true }));
  114. });
  115. expect(onChange).toHaveBeenCalledWith("10240");
  116. unmount();
  117. });
  118. it("switches from custom to inherit - calls onChange with inherit", () => {
  119. const onChange = vi.fn();
  120. const { container, unmount } = render(
  121. <ThinkingBudgetEditor {...defaultProps} value="20000" onChange={onChange} />
  122. );
  123. const select = container.querySelector('[data-testid="select-trigger"]') as HTMLSelectElement;
  124. act(() => {
  125. select.value = "inherit";
  126. select.dispatchEvent(new Event("change", { bubbles: true }));
  127. });
  128. expect(onChange).toHaveBeenCalledWith("inherit");
  129. unmount();
  130. });
  131. it("clicking max-out button calls onChange with 32000", () => {
  132. const onChange = vi.fn();
  133. const { container, unmount } = render(
  134. <ThinkingBudgetEditor {...defaultProps} value="10000" onChange={onChange} />
  135. );
  136. const maxButton = Array.from(container.querySelectorAll("button")).find((b) =>
  137. b.textContent?.includes("maxOutButton")
  138. ) as HTMLButtonElement;
  139. expect(maxButton).toBeTruthy();
  140. act(() => {
  141. maxButton.click();
  142. });
  143. expect(onChange).toHaveBeenCalledWith("32000");
  144. unmount();
  145. });
  146. it("typing a number calls onChange with that value", () => {
  147. const onChange = vi.fn();
  148. const { container, unmount } = render(
  149. <ThinkingBudgetEditor {...defaultProps} value="10000" onChange={onChange} />
  150. );
  151. const input = container.querySelector('input[type="number"]') as HTMLInputElement;
  152. act(() => {
  153. const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
  154. window.HTMLInputElement.prototype,
  155. "value"
  156. )?.set;
  157. nativeInputValueSetter?.call(input, "12345");
  158. input.dispatchEvent(new Event("change", { bubbles: true }));
  159. });
  160. expect(onChange).toHaveBeenCalledWith("12345");
  161. unmount();
  162. });
  163. it("clearing input calls onChange with inherit", () => {
  164. const onChange = vi.fn();
  165. const { container, unmount } = render(
  166. <ThinkingBudgetEditor {...defaultProps} value="10000" onChange={onChange} />
  167. );
  168. const input = container.querySelector('input[type="number"]') as HTMLInputElement;
  169. act(() => {
  170. const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
  171. window.HTMLInputElement.prototype,
  172. "value"
  173. )?.set;
  174. nativeInputValueSetter?.call(input, "");
  175. input.dispatchEvent(new Event("change", { bubbles: true }));
  176. });
  177. expect(onChange).toHaveBeenCalledWith("");
  178. unmount();
  179. });
  180. it("disabled prop disables all controls", () => {
  181. const { container, unmount } = render(
  182. <ThinkingBudgetEditor {...defaultProps} disabled={true} value="10000" />
  183. );
  184. const select = container.querySelector('[data-testid="select-trigger"]') as HTMLSelectElement;
  185. expect(select.disabled).toBe(true);
  186. const input = container.querySelector('input[type="number"]') as HTMLInputElement;
  187. expect(input.disabled).toBe(true);
  188. const maxButton = Array.from(container.querySelectorAll("button")).find((b) =>
  189. b.textContent?.includes("maxOutButton")
  190. ) as HTMLButtonElement;
  191. expect(maxButton).toBeTruthy();
  192. expect(maxButton.disabled).toBe(true);
  193. unmount();
  194. });
  195. });