diff --git a/src/components/CopyButton/__tests__/index.test.tsx b/src/components/CopyButton/__tests__/index.test.tsx
new file mode 100644
index 0000000..64af285
--- /dev/null
+++ b/src/components/CopyButton/__tests__/index.test.tsx
@@ -0,0 +1,194 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import { act, render, screen, waitFor } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+
+// unmock the globally-mocked component so we test the real implementation
+vi.unmock('@/components/CopyButton');
+
+vi.mock('@/utils/clipboard', () => ({
+ copyTextToClipboard: vi.fn(),
+}));
+
+vi.mock('sonner', () => ({
+ toast: {
+ success: vi.fn(),
+ error: vi.fn(),
+ },
+}));
+
+import { CopyButton } from '@/components/CopyButton';
+import { copyTextToClipboard } from '@/utils/clipboard';
+import { toast } from 'sonner';
+
+const mockedCopy = vi.mocked(copyTextToClipboard);
+const mockedToast = vi.mocked(toast);
+
+describe('CopyButton', () => {
+ beforeEach(() => {
+ vi.useFakeTimers({ shouldAdvanceTime: true });
+ vi.clearAllMocks();
+ });
+
+ afterEach(() => {
+ vi.useRealTimers();
+ });
+
+ it('复制成功时调用 copyTextToClipboard 并传入正确 text', async () => {
+ mockedCopy.mockResolvedValue(true);
+ const user = userEvent.setup({ advanceTimers: vi.advanceTimersByTime });
+
+ render(