provider-batch-toolbar.test.tsx 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  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 type { ProviderDisplay, ProviderType } from "@/types/provider";
  8. vi.mock("next-intl", () => ({
  9. useTranslations: () => (key: string, params?: Record<string, unknown>) => {
  10. if (params) {
  11. let result = key;
  12. for (const [k, v] of Object.entries(params)) {
  13. result = result.replace(`{${k}}`, String(v));
  14. }
  15. return result;
  16. }
  17. return key;
  18. },
  19. }));
  20. vi.mock("@/components/ui/button", () => ({
  21. Button: ({ children, ...props }: any) => <button {...props}>{children}</button>,
  22. }));
  23. vi.mock("@/components/ui/checkbox", () => ({
  24. Checkbox: ({ checked, onCheckedChange, ...props }: any) => (
  25. <input
  26. type="checkbox"
  27. checked={checked}
  28. onChange={(e: any) => onCheckedChange?.(e.target.checked)}
  29. {...props}
  30. />
  31. ),
  32. }));
  33. vi.mock("@/components/ui/dropdown-menu", () => ({
  34. DropdownMenu: ({ children }: any) => <div>{children}</div>,
  35. DropdownMenuTrigger: ({ children }: any) => <div>{children}</div>,
  36. DropdownMenuContent: ({ children }: any) => <div>{children}</div>,
  37. DropdownMenuItem: ({ children, onClick }: any) => (
  38. <div role="menuitem" onClick={onClick}>
  39. {children}
  40. </div>
  41. ),
  42. }));
  43. vi.mock("lucide-react", () => ({
  44. ChevronDown: () => <span />,
  45. Pencil: () => <span />,
  46. X: () => <span />,
  47. }));
  48. import {
  49. ProviderBatchToolbar,
  50. type ProviderBatchToolbarProps,
  51. } from "@/app/[locale]/settings/providers/_components/batch-edit/provider-batch-toolbar";
  52. function createProvider(
  53. id: number,
  54. providerType: ProviderType,
  55. groupTag: string | null = null
  56. ): ProviderDisplay {
  57. return { id, providerType, groupTag } as ProviderDisplay;
  58. }
  59. function render(node: React.ReactNode) {
  60. const container = document.createElement("div");
  61. document.body.appendChild(container);
  62. const root = createRoot(container);
  63. act(() => {
  64. root.render(node);
  65. });
  66. return {
  67. container,
  68. unmount: () => {
  69. act(() => root.unmount());
  70. container.remove();
  71. },
  72. };
  73. }
  74. function defaultProps(
  75. overrides: Partial<ProviderBatchToolbarProps> = {}
  76. ): ProviderBatchToolbarProps {
  77. return {
  78. isMultiSelectMode: false,
  79. allSelected: false,
  80. selectedCount: 0,
  81. totalCount: 3,
  82. onEnterMode: vi.fn(),
  83. onExitMode: vi.fn(),
  84. onSelectAll: vi.fn(),
  85. onInvertSelection: vi.fn(),
  86. onOpenBatchEdit: vi.fn(),
  87. providers: [
  88. createProvider(1, "claude"),
  89. createProvider(2, "openai"),
  90. createProvider(3, "claude"),
  91. ],
  92. onSelectByType: vi.fn(),
  93. onSelectByGroup: vi.fn(),
  94. ...overrides,
  95. };
  96. }
  97. describe("ProviderBatchToolbar - discoverability hint", () => {
  98. describe("not in multi-select mode", () => {
  99. it("shows enter-mode button and hint text when totalCount > 1", () => {
  100. const props = defaultProps({ totalCount: 3 });
  101. const { container, unmount } = render(<ProviderBatchToolbar {...props} />);
  102. const buttons = container.querySelectorAll("button");
  103. const enterBtn = Array.from(buttons).find((b) => b.textContent?.includes("enterMode"));
  104. expect(enterBtn).toBeTruthy();
  105. const hint = container.querySelector("span.text-xs");
  106. expect(hint).toBeTruthy();
  107. expect(hint!.textContent).toBe("selectionHint");
  108. unmount();
  109. });
  110. it("shows hint when totalCount is exactly 1 (totalCount > 0 condition)", () => {
  111. const props = defaultProps({
  112. totalCount: 1,
  113. providers: [createProvider(1, "claude")],
  114. });
  115. const { container, unmount } = render(<ProviderBatchToolbar {...props} />);
  116. const hint = container.querySelector("span.text-xs");
  117. expect(hint).toBeTruthy();
  118. unmount();
  119. });
  120. it("does NOT show hint when totalCount is 0", () => {
  121. const props = defaultProps({ totalCount: 0, providers: [] });
  122. const { container, unmount } = render(<ProviderBatchToolbar {...props} />);
  123. const hint = container.querySelector("span.text-xs");
  124. expect(hint).toBeNull();
  125. unmount();
  126. });
  127. it("hint uses i18n key selectionHint", () => {
  128. const props = defaultProps({ totalCount: 5 });
  129. const { container, unmount } = render(<ProviderBatchToolbar {...props} />);
  130. const hint = container.querySelector("span.text-xs");
  131. expect(hint).toBeTruthy();
  132. expect(hint!.textContent).toBe("selectionHint");
  133. unmount();
  134. });
  135. it("enter-mode button is disabled when totalCount is 0", () => {
  136. const props = defaultProps({ totalCount: 0, providers: [] });
  137. const { container, unmount } = render(<ProviderBatchToolbar {...props} />);
  138. const buttons = container.querySelectorAll("button");
  139. const enterBtn = Array.from(buttons).find((b) => b.textContent?.includes("enterMode"));
  140. expect(enterBtn).toBeTruthy();
  141. expect(enterBtn!.disabled).toBe(true);
  142. unmount();
  143. });
  144. });
  145. describe("in multi-select mode", () => {
  146. it("does NOT show hint text", () => {
  147. const props = defaultProps({ isMultiSelectMode: true, selectedCount: 1 });
  148. const { container, unmount } = render(<ProviderBatchToolbar {...props} />);
  149. const allSpans = container.querySelectorAll("span");
  150. const hintSpan = Array.from(allSpans).find((s) => s.textContent === "selectionHint");
  151. expect(hintSpan).toBeFalsy();
  152. unmount();
  153. });
  154. it("renders select-all checkbox and selected count", () => {
  155. const props = defaultProps({ isMultiSelectMode: true, selectedCount: 2 });
  156. const { container, unmount } = render(<ProviderBatchToolbar {...props} />);
  157. const checkbox = container.querySelector('input[type="checkbox"]');
  158. expect(checkbox).toBeTruthy();
  159. const countText = Array.from(container.querySelectorAll("span")).find((s) =>
  160. s.textContent?.includes("selectedCount")
  161. );
  162. expect(countText).toBeTruthy();
  163. unmount();
  164. });
  165. it("renders invert, edit, and exit buttons", () => {
  166. const props = defaultProps({ isMultiSelectMode: true, selectedCount: 1 });
  167. const { container, unmount } = render(<ProviderBatchToolbar {...props} />);
  168. const buttons = container.querySelectorAll("button");
  169. const texts = Array.from(buttons).map((b) => b.textContent);
  170. expect(texts.some((t) => t?.includes("invertSelection"))).toBe(true);
  171. expect(texts.some((t) => t?.includes("editSelected"))).toBe(true);
  172. expect(texts.some((t) => t?.includes("exitMode"))).toBe(true);
  173. unmount();
  174. });
  175. });
  176. });