user-dialogs.test.tsx 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777
  1. /**
  2. * @vitest-environment happy-dom
  3. *
  4. * 单元测试:用户管理 Dialog 组件
  5. *
  6. * 测试对象:
  7. * - EditUserDialog
  8. * - EditKeyDialog
  9. * - AddKeyDialog
  10. * - CreateUserDialog
  11. */
  12. import type { ReactNode } from "react";
  13. import { act } from "react";
  14. import { createRoot } from "react-dom/client";
  15. import { NextIntlClientProvider } from "next-intl";
  16. import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
  17. import { describe, expect, test, vi, beforeEach, afterEach } from "vitest";
  18. // ==================== Mocks ====================
  19. // Mock next/navigation
  20. vi.mock("next/navigation", () => ({
  21. useRouter: () => ({
  22. push: vi.fn(),
  23. refresh: vi.fn(),
  24. replace: vi.fn(),
  25. }),
  26. }));
  27. // Mock @/i18n/routing
  28. vi.mock("@/i18n/routing", () => ({
  29. Link: ({ children }: { children: ReactNode }) => children,
  30. useRouter: () => ({
  31. push: vi.fn(),
  32. refresh: vi.fn(),
  33. replace: vi.fn(),
  34. }),
  35. }));
  36. // Mock Server Actions
  37. const mockEditUser = vi.fn().mockResolvedValue({ ok: true });
  38. const mockRemoveUser = vi.fn().mockResolvedValue({ ok: true });
  39. const mockToggleUserEnabled = vi.fn().mockResolvedValue({ ok: true });
  40. const mockAddKey = vi.fn().mockResolvedValue({ ok: true, data: { key: "sk-test-key" } });
  41. const mockEditKey = vi.fn().mockResolvedValue({ ok: true });
  42. const mockCreateUserOnly = vi.fn().mockResolvedValue({ ok: true, data: { user: { id: 1 } } });
  43. vi.mock("@/actions/users", () => ({
  44. editUser: (...args: unknown[]) => mockEditUser(...args),
  45. removeUser: (...args: unknown[]) => mockRemoveUser(...args),
  46. toggleUserEnabled: (...args: unknown[]) => mockToggleUserEnabled(...args),
  47. createUserOnly: (...args: unknown[]) => mockCreateUserOnly(...args),
  48. }));
  49. vi.mock("@/actions/keys", () => ({
  50. addKey: (...args: unknown[]) => mockAddKey(...args),
  51. editKey: (...args: unknown[]) => mockEditKey(...args),
  52. removeKey: vi.fn().mockResolvedValue({ ok: true }),
  53. }));
  54. vi.mock("@/actions/usage-logs", () => {
  55. return {
  56. getFilterOptions: () => Promise.resolve({ ok: true, data: { models: [] } }),
  57. };
  58. });
  59. // Mock sonner toast
  60. vi.mock("sonner", () => ({
  61. toast: {
  62. success: vi.fn(),
  63. error: vi.fn(),
  64. },
  65. }));
  66. // Mock Dialog components to simplify rendering
  67. vi.mock("@/components/ui/dialog", () => {
  68. type PropsWithChildren = { children?: ReactNode };
  69. type DialogContentProps = PropsWithChildren & { className?: string };
  70. function Dialog({ children }: PropsWithChildren) {
  71. return <div data-testid="dialog-root">{children}</div>;
  72. }
  73. function DialogContent({ children, className }: DialogContentProps) {
  74. return (
  75. <div data-testid="dialog-content" className={className}>
  76. {children}
  77. </div>
  78. );
  79. }
  80. function DialogHeader({ children }: PropsWithChildren) {
  81. return <div data-testid="dialog-header">{children}</div>;
  82. }
  83. function DialogTitle({ children }: PropsWithChildren) {
  84. return <h2 data-testid="dialog-title">{children}</h2>;
  85. }
  86. function DialogDescription({ children, className }: PropsWithChildren & { className?: string }) {
  87. return (
  88. <p data-testid="dialog-description" className={className}>
  89. {children}
  90. </p>
  91. );
  92. }
  93. function DialogFooter({ children, className }: PropsWithChildren & { className?: string }) {
  94. return (
  95. <div data-testid="dialog-footer" className={className}>
  96. {children}
  97. </div>
  98. );
  99. }
  100. return { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter };
  101. });
  102. // Mock form components
  103. vi.mock("@/app/[locale]/dashboard/_components/user/forms/user-edit-section", () => ({
  104. UserEditSection: ({ user, onChange, translations: _translations }: any) => (
  105. <div data-testid="user-edit-section" data-user-id={user?.id}>
  106. <input
  107. data-testid="user-name-input"
  108. value={user?.name || ""}
  109. onChange={(e) => onChange("name", e.target.value)}
  110. />
  111. </div>
  112. ),
  113. }));
  114. vi.mock("@/app/[locale]/dashboard/_components/user/forms/key-edit-section", () => ({
  115. KeyEditSection: ({ keyData, onChange, translations: _translations }: any) => (
  116. <div data-testid="key-edit-section" data-key-id={keyData?.id}>
  117. <input
  118. data-testid="key-name-input"
  119. value={keyData?.name || ""}
  120. onChange={(e) => onChange("name", e.target.value)}
  121. />
  122. </div>
  123. ),
  124. }));
  125. vi.mock("@/app/[locale]/dashboard/_components/user/forms/danger-zone", () => ({
  126. DangerZone: ({ userId, userName, onDelete }: any) => (
  127. <div data-testid="danger-zone" data-user-id={userId}>
  128. <button data-testid="delete-button" onClick={onDelete}>
  129. Delete {userName}
  130. </button>
  131. </div>
  132. ),
  133. }));
  134. vi.mock("@/app/[locale]/dashboard/_components/user/forms/add-key-form", () => ({
  135. AddKeyForm: ({ userId, onSuccess }: any) => (
  136. <div data-testid="add-key-form" data-user-id={userId}>
  137. <button
  138. data-testid="add-key-submit"
  139. onClick={() => onSuccess({ generatedKey: "sk-test", name: "test" })}
  140. >
  141. Add Key
  142. </button>
  143. </div>
  144. ),
  145. }));
  146. vi.mock("@/app/[locale]/dashboard/_components/user/forms/edit-key-form", () => ({
  147. EditKeyForm: ({ keyData, onSuccess }: any) => (
  148. <div data-testid="edit-key-form" data-key-id={keyData?.id}>
  149. <button data-testid="edit-key-submit" onClick={() => onSuccess()}>
  150. Save Key
  151. </button>
  152. </div>
  153. ),
  154. }));
  155. // Import components after mocks
  156. import { EditUserDialog } from "@/app/[locale]/dashboard/_components/user/edit-user-dialog";
  157. import { EditKeyDialog } from "@/app/[locale]/dashboard/_components/user/edit-key-dialog";
  158. import { AddKeyDialog } from "@/app/[locale]/dashboard/_components/user/add-key-dialog";
  159. import { CreateUserDialog } from "@/app/[locale]/dashboard/_components/user/create-user-dialog";
  160. import type { UserDisplay } from "@/types/user";
  161. // ==================== Test Utilities ====================
  162. const messages = {
  163. common: {
  164. save: "Save",
  165. cancel: "Cancel",
  166. close: "Close",
  167. copySuccess: "Copied",
  168. copyFailed: "Copy failed",
  169. },
  170. ui: {
  171. tagInput: {
  172. emptyTag: "Empty tag",
  173. duplicateTag: "Duplicate tag",
  174. tooLong: "Too long",
  175. invalidFormat: "Invalid format",
  176. maxTags: "Too many tags",
  177. },
  178. },
  179. dashboard: {
  180. userManagement: {
  181. editDialog: {
  182. title: "Edit User",
  183. description: "Edit user information",
  184. saving: "Saving...",
  185. saveSuccess: "User saved",
  186. saveFailed: "Save failed",
  187. operationFailed: "Operation failed",
  188. userDisabled: "User disabled",
  189. userEnabled: "User enabled",
  190. deleteFailed: "Delete failed",
  191. userDeleted: "User deleted",
  192. },
  193. createDialog: {
  194. title: "Create User",
  195. description: "Create a new user with API key",
  196. creating: "Creating...",
  197. create: "Create",
  198. saveFailed: "Create failed",
  199. successTitle: "User Created",
  200. successDescription: "User created successfully",
  201. generatedKey: "Generated Key",
  202. keyHint: "Save this key, it cannot be recovered",
  203. },
  204. userEditSection: {
  205. sections: {
  206. basicInfo: "Basic Info",
  207. expireTime: "Expiration",
  208. limitRules: "Limits",
  209. accessRestrictions: "Access",
  210. },
  211. fields: {
  212. username: { label: "Username", placeholder: "Enter username" },
  213. description: { label: "Note", placeholder: "Enter note" },
  214. tags: { label: "Tags", placeholder: "Enter tags" },
  215. providerGroup: { label: "Provider Group", placeholder: "Select group" },
  216. enableStatus: {
  217. label: "Status",
  218. enabledDescription: "Enabled",
  219. disabledDescription: "Disabled",
  220. confirmEnable: "Enable",
  221. confirmDisable: "Disable",
  222. confirmEnableTitle: "Enable User",
  223. confirmDisableTitle: "Disable User",
  224. confirmEnableDescription: "Enable this user?",
  225. confirmDisableDescription: "Disable this user?",
  226. cancel: "Cancel",
  227. processing: "Processing...",
  228. },
  229. allowedClients: {
  230. label: "Allowed Clients",
  231. description: "Restrict clients",
  232. customLabel: "Custom",
  233. customPlaceholder: "Custom client",
  234. customHelp: "Custom help",
  235. },
  236. blockedClients: {
  237. label: "Blocked Clients",
  238. description: "Blocked description",
  239. customLabel: "Custom blocked",
  240. customPlaceholder: "Blocked client",
  241. customHelp: "Blocked help",
  242. },
  243. allowedModels: {
  244. label: "Allowed Models",
  245. placeholder: "Select models",
  246. description: "Restrict models",
  247. },
  248. },
  249. actions: {
  250. allow: "Allow",
  251. block: "Block",
  252. },
  253. presetClients: {
  254. "claude-code": "Claude Code",
  255. "gemini-cli": "Gemini CLI",
  256. "factory-cli": "Factory CLI",
  257. "codex-cli": "Codex CLI",
  258. },
  259. },
  260. keyEditSection: {
  261. sections: {
  262. basicInfo: "Basic Information",
  263. expireTime: "Expiration Time",
  264. limitRules: "Limit Rules",
  265. specialFeatures: "Special Features",
  266. },
  267. fields: {
  268. keyName: { label: "Key Name", placeholder: "Enter key name" },
  269. providerGroup: { label: "Provider Group", placeholder: "Default: default" },
  270. cacheTtl: {
  271. label: "Cache TTL Override",
  272. options: { inherit: "No override", "5m": "5m", "1h": "1h" },
  273. },
  274. balanceQueryPage: {
  275. label: "Independent Personal Usage Page",
  276. description: "When enabled, this key can access an independent personal usage page",
  277. descriptionEnabled: "Enabled description",
  278. descriptionDisabled: "Disabled description",
  279. },
  280. enableStatus: {
  281. label: "Enable Status",
  282. description: "Disabled keys cannot be used",
  283. },
  284. },
  285. },
  286. dangerZone: {
  287. title: "Danger Zone",
  288. deleteUser: "Delete User",
  289. deleteUserDescription: "This action cannot be undone",
  290. deleteConfirm: "Type username to confirm",
  291. deleteButton: "Delete",
  292. },
  293. limitRules: {
  294. addRule: "Add Rule",
  295. ruleTypes: {
  296. limitRpm: "RPM",
  297. limit5h: "5h Limit",
  298. limitDaily: "Daily",
  299. limitWeekly: "Weekly",
  300. limitMonthly: "Monthly",
  301. limitTotal: "Total",
  302. limitSessions: "Sessions",
  303. },
  304. quickValues: {
  305. unlimited: "Unlimited",
  306. "10": "$10",
  307. "50": "$50",
  308. "100": "$100",
  309. "500": "$500",
  310. },
  311. },
  312. quickExpire: {
  313. oneWeek: "1 Week",
  314. oneMonth: "1 Month",
  315. threeMonths: "3 Months",
  316. oneYear: "1 Year",
  317. },
  318. providerGroupSelect: {
  319. providersSuffix: "providers",
  320. loadFailed: "Failed to load provider groups",
  321. },
  322. },
  323. addKeyForm: {
  324. title: "Add Key",
  325. description: "Add a new API key",
  326. successTitle: "Key Created",
  327. successDescription: "Key created successfully",
  328. generatedKey: {
  329. label: "Generated Key",
  330. hint: "Save this key",
  331. },
  332. keyName: {
  333. label: "Key Name",
  334. },
  335. },
  336. },
  337. quota: {
  338. keys: {
  339. editKeyForm: {
  340. title: "Edit Key",
  341. description: "Edit key settings",
  342. },
  343. },
  344. },
  345. };
  346. let queryClient: QueryClient;
  347. function renderWithProviders(node: ReactNode) {
  348. const container = document.createElement("div");
  349. document.body.appendChild(container);
  350. const root = createRoot(container);
  351. act(() => {
  352. root.render(
  353. <QueryClientProvider client={queryClient}>
  354. <NextIntlClientProvider locale="en" messages={messages} timeZone="UTC">
  355. {node}
  356. </NextIntlClientProvider>
  357. </QueryClientProvider>
  358. );
  359. });
  360. return {
  361. container,
  362. unmount: () => {
  363. act(() => root.unmount());
  364. container.remove();
  365. },
  366. };
  367. }
  368. // Mock user data
  369. const mockUser: UserDisplay = {
  370. id: 1,
  371. name: "Test User",
  372. note: "Test note",
  373. role: "user",
  374. rpm: 10,
  375. dailyQuota: 100,
  376. providerGroup: "default",
  377. tags: ["test"],
  378. keys: [],
  379. isEnabled: true,
  380. expiresAt: null,
  381. };
  382. // ==================== Tests ====================
  383. describe("EditUserDialog", () => {
  384. beforeEach(() => {
  385. queryClient = new QueryClient({
  386. defaultOptions: { queries: { retry: false } },
  387. });
  388. vi.clearAllMocks();
  389. });
  390. afterEach(() => {
  391. document.body.innerHTML = "";
  392. });
  393. test("renders dialog with user data when open", () => {
  394. const onOpenChange = vi.fn();
  395. const { container, unmount } = renderWithProviders(
  396. <EditUserDialog open={true} onOpenChange={onOpenChange} user={mockUser} />
  397. );
  398. expect(container.querySelector('[data-testid="dialog-root"]')).not.toBeNull();
  399. expect(container.querySelector('[data-testid="dialog-title"]')?.textContent).toContain(
  400. "Edit User"
  401. );
  402. expect(container.querySelector('[data-testid="user-edit-section"]')).not.toBeNull();
  403. expect(container.querySelector('[data-testid="danger-zone"]')).not.toBeNull();
  404. unmount();
  405. });
  406. test("does not render content when closed", () => {
  407. const onOpenChange = vi.fn();
  408. const { container, unmount } = renderWithProviders(
  409. <EditUserDialog open={false} onOpenChange={onOpenChange} user={mockUser} />
  410. );
  411. // Dialog root exists but content should be minimal
  412. expect(container.querySelector('[data-testid="user-edit-section"]')).toBeNull();
  413. unmount();
  414. });
  415. test("passes correct user id to UserEditSection", () => {
  416. const onOpenChange = vi.fn();
  417. const { container, unmount } = renderWithProviders(
  418. <EditUserDialog open={true} onOpenChange={onOpenChange} user={mockUser} />
  419. );
  420. const userEditSection = container.querySelector('[data-testid="user-edit-section"]');
  421. expect(userEditSection?.getAttribute("data-user-id")).toBe("1");
  422. unmount();
  423. });
  424. test("passes correct user id to DangerZone", () => {
  425. const onOpenChange = vi.fn();
  426. const { container, unmount } = renderWithProviders(
  427. <EditUserDialog open={true} onOpenChange={onOpenChange} user={mockUser} />
  428. );
  429. const dangerZone = container.querySelector('[data-testid="danger-zone"]');
  430. expect(dangerZone?.getAttribute("data-user-id")).toBe("1");
  431. unmount();
  432. });
  433. test("has save and cancel buttons", () => {
  434. const onOpenChange = vi.fn();
  435. const { container, unmount } = renderWithProviders(
  436. <EditUserDialog open={true} onOpenChange={onOpenChange} user={mockUser} />
  437. );
  438. const buttons = container.querySelectorAll("button");
  439. const buttonTexts = Array.from(buttons).map((b) => b.textContent);
  440. expect(buttonTexts).toContain("Save");
  441. expect(buttonTexts).toContain("Cancel");
  442. unmount();
  443. });
  444. });
  445. describe("EditKeyDialog", () => {
  446. beforeEach(() => {
  447. queryClient = new QueryClient({
  448. defaultOptions: { queries: { retry: false } },
  449. });
  450. vi.clearAllMocks();
  451. });
  452. afterEach(() => {
  453. document.body.innerHTML = "";
  454. });
  455. const mockKeyData = {
  456. id: 1,
  457. name: "Test Key",
  458. expiresAt: "2025-12-31",
  459. canLoginWebUi: false,
  460. providerGroup: null,
  461. };
  462. test("renders dialog with key data when open", () => {
  463. const onOpenChange = vi.fn();
  464. const { container, unmount } = renderWithProviders(
  465. <EditKeyDialog open={true} onOpenChange={onOpenChange} keyData={mockKeyData} />
  466. );
  467. expect(container.querySelector('[data-testid="dialog-root"]')).not.toBeNull();
  468. expect(container.querySelector('[data-testid="dialog-title"]')?.textContent).toContain(
  469. "Edit Key"
  470. );
  471. expect(container.querySelector('[data-testid="edit-key-form"]')).not.toBeNull();
  472. unmount();
  473. });
  474. test("passes keyData to EditKeyForm", () => {
  475. const onOpenChange = vi.fn();
  476. const { container, unmount } = renderWithProviders(
  477. <EditKeyDialog open={true} onOpenChange={onOpenChange} keyData={mockKeyData} />
  478. );
  479. const editKeyForm = container.querySelector('[data-testid="edit-key-form"]');
  480. expect(editKeyForm?.getAttribute("data-key-id")).toBe("1");
  481. unmount();
  482. });
  483. test("calls onOpenChange when dialog is closed", () => {
  484. const onOpenChange = vi.fn();
  485. const onSuccess = vi.fn();
  486. const { container, unmount } = renderWithProviders(
  487. <EditKeyDialog
  488. open={true}
  489. onOpenChange={onOpenChange}
  490. keyData={mockKeyData}
  491. onSuccess={onSuccess}
  492. />
  493. );
  494. // Simulate clicking save in the mocked form
  495. const submitButton = container.querySelector('[data-testid="edit-key-submit"]') as HTMLElement;
  496. act(() => {
  497. submitButton?.click();
  498. });
  499. expect(onSuccess).toHaveBeenCalled();
  500. expect(onOpenChange).toHaveBeenCalledWith(false);
  501. unmount();
  502. });
  503. });
  504. describe("AddKeyDialog", () => {
  505. beforeEach(() => {
  506. queryClient = new QueryClient({
  507. defaultOptions: { queries: { retry: false } },
  508. });
  509. vi.clearAllMocks();
  510. });
  511. afterEach(() => {
  512. document.body.innerHTML = "";
  513. });
  514. test("renders dialog with add key form when open", () => {
  515. const onOpenChange = vi.fn();
  516. const { container, unmount } = renderWithProviders(
  517. <AddKeyDialog open={true} onOpenChange={onOpenChange} userId={1} />
  518. );
  519. expect(container.querySelector('[data-testid="dialog-root"]')).not.toBeNull();
  520. expect(container.querySelector('[data-testid="dialog-title"]')?.textContent).toContain(
  521. "Add Key"
  522. );
  523. expect(container.querySelector('[data-testid="add-key-form"]')).not.toBeNull();
  524. unmount();
  525. });
  526. test("passes userId to AddKeyForm", () => {
  527. const onOpenChange = vi.fn();
  528. const { container, unmount } = renderWithProviders(
  529. <AddKeyDialog open={true} onOpenChange={onOpenChange} userId={42} />
  530. );
  531. const addKeyForm = container.querySelector('[data-testid="add-key-form"]');
  532. expect(addKeyForm?.getAttribute("data-user-id")).toBe("42");
  533. unmount();
  534. });
  535. test("calls onSuccess after successful key creation", () => {
  536. const onOpenChange = vi.fn();
  537. const onSuccess = vi.fn();
  538. const { container, unmount } = renderWithProviders(
  539. <AddKeyDialog open={true} onOpenChange={onOpenChange} userId={1} onSuccess={onSuccess} />
  540. );
  541. // Initially shows form
  542. expect(container.querySelector('[data-testid="add-key-form"]')).not.toBeNull();
  543. // Simulate successful key creation
  544. const submitButton = container.querySelector('[data-testid="add-key-submit"]') as HTMLElement;
  545. act(() => {
  546. submitButton?.click();
  547. });
  548. // onSuccess should be called
  549. expect(onSuccess).toHaveBeenCalled();
  550. // The component should now show the success view with generated key info
  551. // (key name "test" from mock result)
  552. expect(container.textContent).toContain("Key Created");
  553. unmount();
  554. });
  555. });
  556. describe("CreateUserDialog", () => {
  557. beforeEach(() => {
  558. queryClient = new QueryClient({
  559. defaultOptions: { queries: { retry: false } },
  560. });
  561. vi.clearAllMocks();
  562. mockCreateUserOnly.mockResolvedValue({ ok: true, data: { user: { id: 1 } } });
  563. mockAddKey.mockResolvedValue({ ok: true, data: { key: "sk-new-user-key" } });
  564. });
  565. afterEach(() => {
  566. document.body.innerHTML = "";
  567. });
  568. test("renders dialog with user and key sections when open", () => {
  569. const onOpenChange = vi.fn();
  570. const { container, unmount } = renderWithProviders(
  571. <CreateUserDialog open={true} onOpenChange={onOpenChange} />
  572. );
  573. expect(container.querySelector('[data-testid="dialog-root"]')).not.toBeNull();
  574. expect(container.querySelector('[data-testid="dialog-title"]')?.textContent).toContain(
  575. "Create User"
  576. );
  577. expect(container.querySelector('[data-testid="user-edit-section"]')).not.toBeNull();
  578. expect(container.querySelector('[data-testid="key-edit-section"]')).not.toBeNull();
  579. unmount();
  580. });
  581. test("does not render content when closed", () => {
  582. const onOpenChange = vi.fn();
  583. const { container, unmount } = renderWithProviders(
  584. <CreateUserDialog open={false} onOpenChange={onOpenChange} />
  585. );
  586. expect(container.querySelector('[data-testid="user-edit-section"]')).toBeNull();
  587. expect(container.querySelector('[data-testid="key-edit-section"]')).toBeNull();
  588. unmount();
  589. });
  590. test("has create and cancel buttons", () => {
  591. const onOpenChange = vi.fn();
  592. const { container, unmount } = renderWithProviders(
  593. <CreateUserDialog open={true} onOpenChange={onOpenChange} />
  594. );
  595. const buttons = container.querySelectorAll("button");
  596. const buttonTexts = Array.from(buttons).map((b) => b.textContent);
  597. expect(buttonTexts).toContain("Create");
  598. expect(buttonTexts).toContain("Cancel");
  599. unmount();
  600. });
  601. });
  602. describe("Dialog Component Integration", () => {
  603. beforeEach(() => {
  604. queryClient = new QueryClient({
  605. defaultOptions: { queries: { retry: false } },
  606. });
  607. vi.clearAllMocks();
  608. });
  609. afterEach(() => {
  610. document.body.innerHTML = "";
  611. });
  612. test("EditUserDialog re-renders with new user when user prop changes", () => {
  613. const onOpenChange = vi.fn();
  614. const { container, unmount } = renderWithProviders(
  615. <EditUserDialog open={true} onOpenChange={onOpenChange} user={mockUser} />
  616. );
  617. // Check initial user
  618. let userEditSection = container.querySelector('[data-testid="user-edit-section"]');
  619. expect(userEditSection?.getAttribute("data-user-id")).toBe("1");
  620. unmount();
  621. // Render with different user
  622. const newUser = { ...mockUser, id: 2, name: "New User" };
  623. const { container: container2, unmount: unmount2 } = renderWithProviders(
  624. <EditUserDialog open={true} onOpenChange={onOpenChange} user={newUser} />
  625. );
  626. userEditSection = container2.querySelector('[data-testid="user-edit-section"]');
  627. expect(userEditSection?.getAttribute("data-user-id")).toBe("2");
  628. unmount2();
  629. });
  630. test("all dialogs have accessible title", () => {
  631. const onOpenChange = vi.fn();
  632. // EditUserDialog
  633. const edit = renderWithProviders(
  634. <EditUserDialog open={true} onOpenChange={onOpenChange} user={mockUser} />
  635. );
  636. expect(edit.container.querySelector('[data-testid="dialog-title"]')).not.toBeNull();
  637. edit.unmount();
  638. // EditKeyDialog
  639. const editKey = renderWithProviders(
  640. <EditKeyDialog
  641. open={true}
  642. onOpenChange={onOpenChange}
  643. keyData={{ id: 1, name: "Key", expiresAt: "" }}
  644. />
  645. );
  646. expect(editKey.container.querySelector('[data-testid="dialog-title"]')).not.toBeNull();
  647. editKey.unmount();
  648. // AddKeyDialog
  649. const addKey = renderWithProviders(
  650. <AddKeyDialog open={true} onOpenChange={onOpenChange} userId={1} />
  651. );
  652. expect(addKey.container.querySelector('[data-testid="dialog-title"]')).not.toBeNull();
  653. addKey.unmount();
  654. // CreateUserDialog
  655. const create = renderWithProviders(
  656. <CreateUserDialog open={true} onOpenChange={onOpenChange} />
  657. );
  658. expect(create.container.querySelector('[data-testid="dialog-title"]')).not.toBeNull();
  659. create.unmount();
  660. });
  661. });