delete-model-dialog.test.tsx 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148
  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 { NextIntlClientProvider } from "next-intl";
  8. import { beforeEach, describe, expect, test, vi } from "vitest";
  9. import { DeleteModelDialog } from "@/app/[locale]/settings/prices/_components/delete-model-dialog";
  10. import { loadMessages } from "./test-messages";
  11. const modelPricesActionMocks = vi.hoisted(() => ({
  12. deleteSingleModelPrice: vi.fn(async () => ({ ok: true, data: null })),
  13. }));
  14. vi.mock("@/actions/model-prices", () => modelPricesActionMocks);
  15. const sonnerMocks = vi.hoisted(() => ({
  16. toast: {
  17. success: vi.fn(),
  18. error: vi.fn(),
  19. },
  20. }));
  21. vi.mock("sonner", () => sonnerMocks);
  22. function render(node: ReactNode) {
  23. const container = document.createElement("div");
  24. document.body.appendChild(container);
  25. const root = createRoot(container);
  26. act(() => {
  27. root.render(node);
  28. });
  29. return {
  30. unmount: () => {
  31. act(() => root.unmount());
  32. container.remove();
  33. },
  34. };
  35. }
  36. async function flushPromises() {
  37. await new Promise((resolve) => setTimeout(resolve, 0));
  38. }
  39. describe("DeleteModelDialog: 删除流程", () => {
  40. beforeEach(() => {
  41. vi.clearAllMocks();
  42. document.body.innerHTML = "";
  43. });
  44. test("删除成功:应调用 action、提示成功、触发回调与事件", async () => {
  45. const messages = loadMessages();
  46. const onSuccess = vi.fn();
  47. const priceUpdatedListener = vi.fn();
  48. window.addEventListener("price-data-updated", priceUpdatedListener);
  49. const { unmount } = render(
  50. <NextIntlClientProvider locale="en" messages={messages}>
  51. <DeleteModelDialog modelName="model-to-delete" onSuccess={onSuccess} />
  52. </NextIntlClientProvider>
  53. );
  54. const trigger = Array.from(document.querySelectorAll("button")).find(
  55. (el) => el.textContent?.trim() === "Delete"
  56. );
  57. expect(trigger).toBeTruthy();
  58. await act(async () => {
  59. trigger?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
  60. await flushPromises();
  61. });
  62. const dialog = document.querySelector(
  63. '[data-slot="alert-dialog-content"]'
  64. ) as HTMLElement | null;
  65. expect(dialog).toBeTruthy();
  66. const confirmButton = Array.from(dialog!.querySelectorAll("button")).find(
  67. (el) => el.textContent?.trim() === "Delete"
  68. );
  69. expect(confirmButton).toBeTruthy();
  70. await act(async () => {
  71. confirmButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
  72. await flushPromises();
  73. await flushPromises();
  74. });
  75. expect(modelPricesActionMocks.deleteSingleModelPrice).toHaveBeenCalledWith("model-to-delete");
  76. expect(sonnerMocks.toast.success).toHaveBeenCalled();
  77. expect(sonnerMocks.toast.error).not.toHaveBeenCalled();
  78. expect(onSuccess).toHaveBeenCalled();
  79. expect(priceUpdatedListener).toHaveBeenCalled();
  80. window.removeEventListener("price-data-updated", priceUpdatedListener);
  81. unmount();
  82. });
  83. test("删除失败:应提示错误且不触发成功回调", async () => {
  84. modelPricesActionMocks.deleteSingleModelPrice.mockResolvedValueOnce({
  85. ok: false,
  86. error: "bad",
  87. });
  88. const messages = loadMessages();
  89. const onSuccess = vi.fn();
  90. const { unmount } = render(
  91. <NextIntlClientProvider locale="en" messages={messages}>
  92. <DeleteModelDialog modelName="model-to-delete" onSuccess={onSuccess} />
  93. </NextIntlClientProvider>
  94. );
  95. const trigger = Array.from(document.querySelectorAll("button")).find(
  96. (el) => el.textContent?.trim() === "Delete"
  97. );
  98. expect(trigger).toBeTruthy();
  99. await act(async () => {
  100. trigger?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
  101. await flushPromises();
  102. });
  103. const dialog = document.querySelector(
  104. '[data-slot="alert-dialog-content"]'
  105. ) as HTMLElement | null;
  106. expect(dialog).toBeTruthy();
  107. const confirmButton = Array.from(dialog!.querySelectorAll("button")).find(
  108. (el) => el.textContent?.trim() === "Delete"
  109. );
  110. expect(confirmButton).toBeTruthy();
  111. await act(async () => {
  112. confirmButton?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
  113. await flushPromises();
  114. await flushPromises();
  115. });
  116. expect(modelPricesActionMocks.deleteSingleModelPrice).toHaveBeenCalledWith("model-to-delete");
  117. expect(sonnerMocks.toast.error).toHaveBeenCalledWith("bad");
  118. expect(onSuccess).not.toHaveBeenCalled();
  119. unmount();
  120. });
  121. });