user-dialogs.test.tsx 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765
  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. },
  235. allowedModels: {
  236. label: "Allowed Models",
  237. placeholder: "Select models",
  238. description: "Restrict models",
  239. },
  240. },
  241. presetClients: {
  242. "claude-cli": "Claude CLI",
  243. "gemini-cli": "Gemini CLI",
  244. "factory-cli": "Factory CLI",
  245. "codex-cli": "Codex CLI",
  246. },
  247. },
  248. keyEditSection: {
  249. sections: {
  250. basicInfo: "Basic Information",
  251. expireTime: "Expiration Time",
  252. limitRules: "Limit Rules",
  253. specialFeatures: "Special Features",
  254. },
  255. fields: {
  256. keyName: { label: "Key Name", placeholder: "Enter key name" },
  257. providerGroup: { label: "Provider Group", placeholder: "Default: default" },
  258. cacheTtl: {
  259. label: "Cache TTL Override",
  260. options: { inherit: "No override", "5m": "5m", "1h": "1h" },
  261. },
  262. balanceQueryPage: {
  263. label: "Independent Personal Usage Page",
  264. description: "When enabled, this key can access an independent personal usage page",
  265. descriptionEnabled: "Enabled description",
  266. descriptionDisabled: "Disabled description",
  267. },
  268. enableStatus: {
  269. label: "Enable Status",
  270. description: "Disabled keys cannot be used",
  271. },
  272. },
  273. },
  274. dangerZone: {
  275. title: "Danger Zone",
  276. deleteUser: "Delete User",
  277. deleteUserDescription: "This action cannot be undone",
  278. deleteConfirm: "Type username to confirm",
  279. deleteButton: "Delete",
  280. },
  281. limitRules: {
  282. addRule: "Add Rule",
  283. ruleTypes: {
  284. limitRpm: "RPM",
  285. limit5h: "5h Limit",
  286. limitDaily: "Daily",
  287. limitWeekly: "Weekly",
  288. limitMonthly: "Monthly",
  289. limitTotal: "Total",
  290. limitSessions: "Sessions",
  291. },
  292. quickValues: {
  293. unlimited: "Unlimited",
  294. "10": "$10",
  295. "50": "$50",
  296. "100": "$100",
  297. "500": "$500",
  298. },
  299. },
  300. quickExpire: {
  301. oneWeek: "1 Week",
  302. oneMonth: "1 Month",
  303. threeMonths: "3 Months",
  304. oneYear: "1 Year",
  305. },
  306. providerGroupSelect: {
  307. providersSuffix: "providers",
  308. loadFailed: "Failed to load provider groups",
  309. },
  310. },
  311. addKeyForm: {
  312. title: "Add Key",
  313. description: "Add a new API key",
  314. successTitle: "Key Created",
  315. successDescription: "Key created successfully",
  316. generatedKey: {
  317. label: "Generated Key",
  318. hint: "Save this key",
  319. },
  320. keyName: {
  321. label: "Key Name",
  322. },
  323. },
  324. },
  325. quota: {
  326. keys: {
  327. editKeyForm: {
  328. title: "Edit Key",
  329. description: "Edit key settings",
  330. },
  331. },
  332. },
  333. };
  334. let queryClient: QueryClient;
  335. function renderWithProviders(node: ReactNode) {
  336. const container = document.createElement("div");
  337. document.body.appendChild(container);
  338. const root = createRoot(container);
  339. act(() => {
  340. root.render(
  341. <QueryClientProvider client={queryClient}>
  342. <NextIntlClientProvider locale="en" messages={messages} timeZone="UTC">
  343. {node}
  344. </NextIntlClientProvider>
  345. </QueryClientProvider>
  346. );
  347. });
  348. return {
  349. container,
  350. unmount: () => {
  351. act(() => root.unmount());
  352. container.remove();
  353. },
  354. };
  355. }
  356. // Mock user data
  357. const mockUser: UserDisplay = {
  358. id: 1,
  359. name: "Test User",
  360. note: "Test note",
  361. role: "user",
  362. rpm: 10,
  363. dailyQuota: 100,
  364. providerGroup: "default",
  365. tags: ["test"],
  366. keys: [],
  367. isEnabled: true,
  368. expiresAt: null,
  369. };
  370. // ==================== Tests ====================
  371. describe("EditUserDialog", () => {
  372. beforeEach(() => {
  373. queryClient = new QueryClient({
  374. defaultOptions: { queries: { retry: false } },
  375. });
  376. vi.clearAllMocks();
  377. });
  378. afterEach(() => {
  379. document.body.innerHTML = "";
  380. });
  381. test("renders dialog with user data when open", () => {
  382. const onOpenChange = vi.fn();
  383. const { container, unmount } = renderWithProviders(
  384. <EditUserDialog open={true} onOpenChange={onOpenChange} user={mockUser} />
  385. );
  386. expect(container.querySelector('[data-testid="dialog-root"]')).not.toBeNull();
  387. expect(container.querySelector('[data-testid="dialog-title"]')?.textContent).toContain(
  388. "Edit User"
  389. );
  390. expect(container.querySelector('[data-testid="user-edit-section"]')).not.toBeNull();
  391. expect(container.querySelector('[data-testid="danger-zone"]')).not.toBeNull();
  392. unmount();
  393. });
  394. test("does not render content when closed", () => {
  395. const onOpenChange = vi.fn();
  396. const { container, unmount } = renderWithProviders(
  397. <EditUserDialog open={false} onOpenChange={onOpenChange} user={mockUser} />
  398. );
  399. // Dialog root exists but content should be minimal
  400. expect(container.querySelector('[data-testid="user-edit-section"]')).toBeNull();
  401. unmount();
  402. });
  403. test("passes correct user id to UserEditSection", () => {
  404. const onOpenChange = vi.fn();
  405. const { container, unmount } = renderWithProviders(
  406. <EditUserDialog open={true} onOpenChange={onOpenChange} user={mockUser} />
  407. );
  408. const userEditSection = container.querySelector('[data-testid="user-edit-section"]');
  409. expect(userEditSection?.getAttribute("data-user-id")).toBe("1");
  410. unmount();
  411. });
  412. test("passes correct user id to DangerZone", () => {
  413. const onOpenChange = vi.fn();
  414. const { container, unmount } = renderWithProviders(
  415. <EditUserDialog open={true} onOpenChange={onOpenChange} user={mockUser} />
  416. );
  417. const dangerZone = container.querySelector('[data-testid="danger-zone"]');
  418. expect(dangerZone?.getAttribute("data-user-id")).toBe("1");
  419. unmount();
  420. });
  421. test("has save and cancel buttons", () => {
  422. const onOpenChange = vi.fn();
  423. const { container, unmount } = renderWithProviders(
  424. <EditUserDialog open={true} onOpenChange={onOpenChange} user={mockUser} />
  425. );
  426. const buttons = container.querySelectorAll("button");
  427. const buttonTexts = Array.from(buttons).map((b) => b.textContent);
  428. expect(buttonTexts).toContain("Save");
  429. expect(buttonTexts).toContain("Cancel");
  430. unmount();
  431. });
  432. });
  433. describe("EditKeyDialog", () => {
  434. beforeEach(() => {
  435. queryClient = new QueryClient({
  436. defaultOptions: { queries: { retry: false } },
  437. });
  438. vi.clearAllMocks();
  439. });
  440. afterEach(() => {
  441. document.body.innerHTML = "";
  442. });
  443. const mockKeyData = {
  444. id: 1,
  445. name: "Test Key",
  446. expiresAt: "2025-12-31",
  447. canLoginWebUi: false,
  448. providerGroup: null,
  449. };
  450. test("renders dialog with key data when open", () => {
  451. const onOpenChange = vi.fn();
  452. const { container, unmount } = renderWithProviders(
  453. <EditKeyDialog open={true} onOpenChange={onOpenChange} keyData={mockKeyData} />
  454. );
  455. expect(container.querySelector('[data-testid="dialog-root"]')).not.toBeNull();
  456. expect(container.querySelector('[data-testid="dialog-title"]')?.textContent).toContain(
  457. "Edit Key"
  458. );
  459. expect(container.querySelector('[data-testid="edit-key-form"]')).not.toBeNull();
  460. unmount();
  461. });
  462. test("passes keyData to EditKeyForm", () => {
  463. const onOpenChange = vi.fn();
  464. const { container, unmount } = renderWithProviders(
  465. <EditKeyDialog open={true} onOpenChange={onOpenChange} keyData={mockKeyData} />
  466. );
  467. const editKeyForm = container.querySelector('[data-testid="edit-key-form"]');
  468. expect(editKeyForm?.getAttribute("data-key-id")).toBe("1");
  469. unmount();
  470. });
  471. test("calls onOpenChange when dialog is closed", () => {
  472. const onOpenChange = vi.fn();
  473. const onSuccess = vi.fn();
  474. const { container, unmount } = renderWithProviders(
  475. <EditKeyDialog
  476. open={true}
  477. onOpenChange={onOpenChange}
  478. keyData={mockKeyData}
  479. onSuccess={onSuccess}
  480. />
  481. );
  482. // Simulate clicking save in the mocked form
  483. const submitButton = container.querySelector('[data-testid="edit-key-submit"]') as HTMLElement;
  484. act(() => {
  485. submitButton?.click();
  486. });
  487. expect(onSuccess).toHaveBeenCalled();
  488. expect(onOpenChange).toHaveBeenCalledWith(false);
  489. unmount();
  490. });
  491. });
  492. describe("AddKeyDialog", () => {
  493. beforeEach(() => {
  494. queryClient = new QueryClient({
  495. defaultOptions: { queries: { retry: false } },
  496. });
  497. vi.clearAllMocks();
  498. });
  499. afterEach(() => {
  500. document.body.innerHTML = "";
  501. });
  502. test("renders dialog with add key form when open", () => {
  503. const onOpenChange = vi.fn();
  504. const { container, unmount } = renderWithProviders(
  505. <AddKeyDialog open={true} onOpenChange={onOpenChange} userId={1} />
  506. );
  507. expect(container.querySelector('[data-testid="dialog-root"]')).not.toBeNull();
  508. expect(container.querySelector('[data-testid="dialog-title"]')?.textContent).toContain(
  509. "Add Key"
  510. );
  511. expect(container.querySelector('[data-testid="add-key-form"]')).not.toBeNull();
  512. unmount();
  513. });
  514. test("passes userId to AddKeyForm", () => {
  515. const onOpenChange = vi.fn();
  516. const { container, unmount } = renderWithProviders(
  517. <AddKeyDialog open={true} onOpenChange={onOpenChange} userId={42} />
  518. );
  519. const addKeyForm = container.querySelector('[data-testid="add-key-form"]');
  520. expect(addKeyForm?.getAttribute("data-user-id")).toBe("42");
  521. unmount();
  522. });
  523. test("calls onSuccess after successful key creation", () => {
  524. const onOpenChange = vi.fn();
  525. const onSuccess = vi.fn();
  526. const { container, unmount } = renderWithProviders(
  527. <AddKeyDialog open={true} onOpenChange={onOpenChange} userId={1} onSuccess={onSuccess} />
  528. );
  529. // Initially shows form
  530. expect(container.querySelector('[data-testid="add-key-form"]')).not.toBeNull();
  531. // Simulate successful key creation
  532. const submitButton = container.querySelector('[data-testid="add-key-submit"]') as HTMLElement;
  533. act(() => {
  534. submitButton?.click();
  535. });
  536. // onSuccess should be called
  537. expect(onSuccess).toHaveBeenCalled();
  538. // The component should now show the success view with generated key info
  539. // (key name "test" from mock result)
  540. expect(container.textContent).toContain("Key Created");
  541. unmount();
  542. });
  543. });
  544. describe("CreateUserDialog", () => {
  545. beforeEach(() => {
  546. queryClient = new QueryClient({
  547. defaultOptions: { queries: { retry: false } },
  548. });
  549. vi.clearAllMocks();
  550. mockCreateUserOnly.mockResolvedValue({ ok: true, data: { user: { id: 1 } } });
  551. mockAddKey.mockResolvedValue({ ok: true, data: { key: "sk-new-user-key" } });
  552. });
  553. afterEach(() => {
  554. document.body.innerHTML = "";
  555. });
  556. test("renders dialog with user and key sections when open", () => {
  557. const onOpenChange = vi.fn();
  558. const { container, unmount } = renderWithProviders(
  559. <CreateUserDialog open={true} onOpenChange={onOpenChange} />
  560. );
  561. expect(container.querySelector('[data-testid="dialog-root"]')).not.toBeNull();
  562. expect(container.querySelector('[data-testid="dialog-title"]')?.textContent).toContain(
  563. "Create User"
  564. );
  565. expect(container.querySelector('[data-testid="user-edit-section"]')).not.toBeNull();
  566. expect(container.querySelector('[data-testid="key-edit-section"]')).not.toBeNull();
  567. unmount();
  568. });
  569. test("does not render content when closed", () => {
  570. const onOpenChange = vi.fn();
  571. const { container, unmount } = renderWithProviders(
  572. <CreateUserDialog open={false} onOpenChange={onOpenChange} />
  573. );
  574. expect(container.querySelector('[data-testid="user-edit-section"]')).toBeNull();
  575. expect(container.querySelector('[data-testid="key-edit-section"]')).toBeNull();
  576. unmount();
  577. });
  578. test("has create and cancel buttons", () => {
  579. const onOpenChange = vi.fn();
  580. const { container, unmount } = renderWithProviders(
  581. <CreateUserDialog open={true} onOpenChange={onOpenChange} />
  582. );
  583. const buttons = container.querySelectorAll("button");
  584. const buttonTexts = Array.from(buttons).map((b) => b.textContent);
  585. expect(buttonTexts).toContain("Create");
  586. expect(buttonTexts).toContain("Cancel");
  587. unmount();
  588. });
  589. });
  590. describe("Dialog Component Integration", () => {
  591. beforeEach(() => {
  592. queryClient = new QueryClient({
  593. defaultOptions: { queries: { retry: false } },
  594. });
  595. vi.clearAllMocks();
  596. });
  597. afterEach(() => {
  598. document.body.innerHTML = "";
  599. });
  600. test("EditUserDialog re-renders with new user when user prop changes", () => {
  601. const onOpenChange = vi.fn();
  602. const { container, unmount } = renderWithProviders(
  603. <EditUserDialog open={true} onOpenChange={onOpenChange} user={mockUser} />
  604. );
  605. // Check initial user
  606. let userEditSection = container.querySelector('[data-testid="user-edit-section"]');
  607. expect(userEditSection?.getAttribute("data-user-id")).toBe("1");
  608. unmount();
  609. // Render with different user
  610. const newUser = { ...mockUser, id: 2, name: "New User" };
  611. const { container: container2, unmount: unmount2 } = renderWithProviders(
  612. <EditUserDialog open={true} onOpenChange={onOpenChange} user={newUser} />
  613. );
  614. userEditSection = container2.querySelector('[data-testid="user-edit-section"]');
  615. expect(userEditSection?.getAttribute("data-user-id")).toBe("2");
  616. unmount2();
  617. });
  618. test("all dialogs have accessible title", () => {
  619. const onOpenChange = vi.fn();
  620. // EditUserDialog
  621. const edit = renderWithProviders(
  622. <EditUserDialog open={true} onOpenChange={onOpenChange} user={mockUser} />
  623. );
  624. expect(edit.container.querySelector('[data-testid="dialog-title"]')).not.toBeNull();
  625. edit.unmount();
  626. // EditKeyDialog
  627. const editKey = renderWithProviders(
  628. <EditKeyDialog
  629. open={true}
  630. onOpenChange={onOpenChange}
  631. keyData={{ id: 1, name: "Key", expiresAt: "" }}
  632. />
  633. );
  634. expect(editKey.container.querySelector('[data-testid="dialog-title"]')).not.toBeNull();
  635. editKey.unmount();
  636. // AddKeyDialog
  637. const addKey = renderWithProviders(
  638. <AddKeyDialog open={true} onOpenChange={onOpenChange} userId={1} />
  639. );
  640. expect(addKey.container.querySelector('[data-testid="dialog-title"]')).not.toBeNull();
  641. addKey.unmount();
  642. // CreateUserDialog
  643. const create = renderWithProviders(
  644. <CreateUserDialog open={true} onOpenChange={onOpenChange} />
  645. );
  646. expect(create.container.querySelector('[data-testid="dialog-title"]')).not.toBeNull();
  647. create.unmount();
  648. });
  649. });