batch-key-section-toggle.test.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. /**
  2. * @vitest-environment happy-dom
  3. *
  4. * BatchKeySection: canLoginWebUi toggle semantics alignment test
  5. * Verifies that the toggle uses inverted logic to match add/edit forms:
  6. * - Switch ON => canLoginWebUi=false (independent personal usage page)
  7. * - Switch OFF => canLoginWebUi=true (restricted Web UI)
  8. */
  9. import type { ReactNode } from "react";
  10. import { act } from "react";
  11. import { createRoot } from "react-dom/client";
  12. import { beforeEach, describe, expect, test, vi } from "vitest";
  13. import {
  14. BatchKeySection,
  15. type BatchKeySectionState,
  16. } from "@/app/[locale]/dashboard/_components/user/batch-edit/batch-key-section";
  17. function render(node: ReactNode) {
  18. const container = document.createElement("div");
  19. document.body.appendChild(container);
  20. const root = createRoot(container);
  21. act(() => {
  22. root.render(node);
  23. });
  24. return {
  25. container,
  26. unmount: () => {
  27. act(() => root.unmount());
  28. container.remove();
  29. },
  30. };
  31. }
  32. const createInitialState = (): BatchKeySectionState => ({
  33. providerGroupEnabled: false,
  34. providerGroup: "",
  35. limit5hUsdEnabled: false,
  36. limit5hUsd: "",
  37. limitDailyUsdEnabled: false,
  38. limitDailyUsd: "",
  39. limitWeeklyUsdEnabled: false,
  40. limitWeeklyUsd: "",
  41. limitMonthlyUsdEnabled: false,
  42. limitMonthlyUsd: "",
  43. canLoginWebUiEnabled: true, // Enable the field for testing
  44. canLoginWebUi: false, // Default: independent page enabled (switch ON)
  45. isEnabledEnabled: false,
  46. isEnabled: true,
  47. });
  48. const createTranslations = () => ({
  49. title: "Key Settings",
  50. affected: "Will affect {count} keys",
  51. enableFieldAria: "Enable {title} field",
  52. fields: {
  53. providerGroup: "Group (providerGroup)",
  54. limit5h: "5h Limit (USD)",
  55. limitDaily: "Daily Limit (USD)",
  56. limitWeekly: "Weekly Limit (USD)",
  57. limitMonthly: "Monthly Limit (USD)",
  58. canLoginWebUi: "Independent Personal Usage Page", // Updated label
  59. keyEnabled: "Key Enabled Status",
  60. },
  61. placeholders: {
  62. groupPlaceholder: "Leave empty to clear",
  63. emptyNoLimit: "Leave empty for no limit",
  64. },
  65. targetValue: "Target Value",
  66. });
  67. // Helper to find the inner value switch (not the enable/disable switch)
  68. // The inner switch has aria-label starting with "Target Value:"
  69. function findCanLoginWebUiValueSwitch(): HTMLButtonElement | null {
  70. const switches = document.body.querySelectorAll('button[role="switch"]');
  71. return Array.from(switches).find((el) =>
  72. el.getAttribute("aria-label")?.startsWith("Target Value: Independent Personal Usage Page")
  73. ) as HTMLButtonElement | null;
  74. }
  75. describe("BatchKeySection: canLoginWebUi toggle semantics alignment", () => {
  76. beforeEach(() => {
  77. while (document.body.firstChild) {
  78. document.body.removeChild(document.body.firstChild);
  79. }
  80. });
  81. test("should use 'Independent Personal Usage Page' label for canLoginWebUi field", () => {
  82. const state = createInitialState();
  83. const translations = createTranslations();
  84. const onChange = vi.fn();
  85. const { unmount } = render(
  86. <BatchKeySection
  87. affectedKeysCount={5}
  88. state={state}
  89. onChange={onChange}
  90. translations={translations}
  91. />
  92. );
  93. // Find the field card title for canLoginWebUi
  94. const fieldCards = document.body.querySelectorAll(".text-sm.font-medium");
  95. const canLoginWebUiCard = Array.from(fieldCards).find(
  96. (el) => el.textContent === "Independent Personal Usage Page"
  97. );
  98. expect(canLoginWebUiCard).toBeTruthy();
  99. unmount();
  100. });
  101. test("switch should be ON (checked) when canLoginWebUi=false (inverted logic)", () => {
  102. const state = createInitialState();
  103. state.canLoginWebUi = false; // Independent page enabled
  104. const translations = createTranslations();
  105. const onChange = vi.fn();
  106. const { unmount } = render(
  107. <BatchKeySection
  108. affectedKeysCount={5}
  109. state={state}
  110. onChange={onChange}
  111. translations={translations}
  112. />
  113. );
  114. const canLoginWebUiSwitch = findCanLoginWebUiValueSwitch();
  115. expect(canLoginWebUiSwitch).toBeTruthy();
  116. // With inverted logic: canLoginWebUi=false should show switch as ON (checked)
  117. expect(canLoginWebUiSwitch?.getAttribute("data-state")).toBe("checked");
  118. unmount();
  119. });
  120. test("switch should be OFF (unchecked) when canLoginWebUi=true (inverted logic)", () => {
  121. const state = createInitialState();
  122. state.canLoginWebUi = true; // Restricted Web UI
  123. const translations = createTranslations();
  124. const onChange = vi.fn();
  125. const { unmount } = render(
  126. <BatchKeySection
  127. affectedKeysCount={5}
  128. state={state}
  129. onChange={onChange}
  130. translations={translations}
  131. />
  132. );
  133. const canLoginWebUiSwitch = findCanLoginWebUiValueSwitch();
  134. expect(canLoginWebUiSwitch).toBeTruthy();
  135. // With inverted logic: canLoginWebUi=true should show switch as OFF (unchecked)
  136. expect(canLoginWebUiSwitch?.getAttribute("data-state")).toBe("unchecked");
  137. unmount();
  138. });
  139. test("toggling switch ON should set canLoginWebUi=false (inverted logic)", async () => {
  140. const state = createInitialState();
  141. state.canLoginWebUi = true; // Start with restricted Web UI (switch OFF)
  142. const translations = createTranslations();
  143. const onChange = vi.fn();
  144. const { unmount } = render(
  145. <BatchKeySection
  146. affectedKeysCount={5}
  147. state={state}
  148. onChange={onChange}
  149. translations={translations}
  150. />
  151. );
  152. const canLoginWebUiSwitch = findCanLoginWebUiValueSwitch();
  153. expect(canLoginWebUiSwitch).toBeTruthy();
  154. // Click to toggle ON
  155. await act(async () => {
  156. canLoginWebUiSwitch?.click();
  157. await new Promise((r) => setTimeout(r, 50));
  158. });
  159. // With inverted logic: clicking switch ON should set canLoginWebUi=false
  160. expect(onChange).toHaveBeenCalledWith({ canLoginWebUi: false });
  161. unmount();
  162. });
  163. test("toggling switch OFF should set canLoginWebUi=true (inverted logic)", async () => {
  164. const state = createInitialState();
  165. state.canLoginWebUi = false; // Start with independent page (switch ON)
  166. const translations = createTranslations();
  167. const onChange = vi.fn();
  168. const { unmount } = render(
  169. <BatchKeySection
  170. affectedKeysCount={5}
  171. state={state}
  172. onChange={onChange}
  173. translations={translations}
  174. />
  175. );
  176. const canLoginWebUiSwitch = findCanLoginWebUiValueSwitch();
  177. expect(canLoginWebUiSwitch).toBeTruthy();
  178. // Click to toggle OFF
  179. await act(async () => {
  180. canLoginWebUiSwitch?.click();
  181. await new Promise((r) => setTimeout(r, 50));
  182. });
  183. // With inverted logic: clicking switch OFF should set canLoginWebUi=true
  184. expect(onChange).toHaveBeenCalledWith({ canLoginWebUi: true });
  185. unmount();
  186. });
  187. });