client-restrictions-editor.test.tsx 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. /**
  2. * @vitest-environment happy-dom
  3. */
  4. import type { ReactNode } from "react";
  5. import { act } from "react";
  6. import { createRoot } from "react-dom/client";
  7. import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
  8. vi.mock("@/lib/client-restrictions/client-presets", async (importOriginal) => {
  9. const actual = await importOriginal<typeof import("@/lib/client-restrictions/client-presets")>();
  10. return {
  11. ...actual,
  12. CLIENT_RESTRICTION_PRESET_OPTIONS: [],
  13. };
  14. });
  15. vi.mock("@/components/ui/tag-input", () => ({
  16. TagInput: vi.fn(() => null),
  17. }));
  18. // eslint-disable-next-line import/order -- must come after vi.mock
  19. import { TagInput } from "@/components/ui/tag-input";
  20. // eslint-disable-next-line import/order -- must come after vi.mock
  21. import { ClientRestrictionsEditor } from "@/components/form/client-restrictions-editor";
  22. function render(node: ReactNode) {
  23. const container = document.createElement("div");
  24. document.body.appendChild(container);
  25. const root = createRoot(container);
  26. act(() => root.render(node));
  27. return () => {
  28. act(() => root.unmount());
  29. container.remove();
  30. };
  31. }
  32. type TagInputProps = { onChange: (v: string[]) => void; value: string[] };
  33. function getTagInputProps(callIndex: number): TagInputProps {
  34. const calls = vi.mocked(TagInput).mock.calls;
  35. const call = calls[callIndex];
  36. if (!call) throw new Error(`TagInput call ${callIndex} not found (got ${calls.length} calls)`);
  37. return call[0] as TagInputProps;
  38. }
  39. function getTagInputOnChange(callIndex: number): (values: string[]) => void {
  40. return getTagInputProps(callIndex).onChange;
  41. }
  42. describe("ClientRestrictionsEditor", () => {
  43. const onAllowedChange = vi.fn();
  44. const onBlockedChange = vi.fn();
  45. beforeEach(() => {
  46. vi.clearAllMocks();
  47. });
  48. afterEach(() => {
  49. while (document.body.firstChild) {
  50. document.body.removeChild(document.body.firstChild);
  51. }
  52. });
  53. function renderEditor(allowed: string[], blocked: string[]) {
  54. return render(
  55. <ClientRestrictionsEditor
  56. allowed={allowed}
  57. blocked={blocked}
  58. onAllowedChange={onAllowedChange}
  59. onBlockedChange={onBlockedChange}
  60. translations={{
  61. allowAction: "允许",
  62. blockAction: "阻止",
  63. customAllowedLabel: "自定义允许",
  64. customAllowedPlaceholder: "",
  65. customBlockedLabel: "自定义阻止",
  66. customBlockedPlaceholder: "",
  67. customHelp: "",
  68. presetClients: {},
  69. }}
  70. />
  71. );
  72. }
  73. describe("uniqueOrdered normalization", () => {
  74. it("deduplicates values preserving first occurrence order", () => {
  75. const unmount = renderEditor([], []);
  76. act(() => getTagInputOnChange(0)(["a", "b", "a", "c"]));
  77. expect(onAllowedChange).toHaveBeenCalledWith(["a", "b", "c"]);
  78. unmount();
  79. });
  80. it("preserves preset aliases and filters them out from custom input", () => {
  81. const unmount = renderEditor(["claude-code-cli", "my-ide"], []);
  82. expect(getTagInputProps(0).value).toEqual(["my-ide"]);
  83. act(() => getTagInputOnChange(0)(["next-ide", "claude-code-cli"]));
  84. expect(onAllowedChange).toHaveBeenCalledWith(["claude-code-cli", "next-ide"]);
  85. unmount();
  86. });
  87. it("does not change blocked values when editing allowed custom values", () => {
  88. const unmount = renderEditor([], ["b", "c"]);
  89. act(() => getTagInputOnChange(0)(["a", "b"]));
  90. expect(onAllowedChange).toHaveBeenCalledWith(["a", "b"]);
  91. expect(onBlockedChange).not.toHaveBeenCalled();
  92. unmount();
  93. });
  94. });
  95. describe("custom blocked field", () => {
  96. it("preserves preset aliases and filters them out from custom input", () => {
  97. const unmount = renderEditor([], ["claude-code-vscode", "blocked-ide"]);
  98. expect(getTagInputProps(1).value).toEqual(["blocked-ide"]);
  99. act(() => getTagInputOnChange(1)(["next-blocked", "claude-code-vscode"]));
  100. expect(onBlockedChange).toHaveBeenCalledWith(["claude-code-vscode", "next-blocked"]);
  101. expect(onAllowedChange).not.toHaveBeenCalled();
  102. unmount();
  103. });
  104. });
  105. });