diff --git a/entrypoints/options/__tests__/App.test.tsx b/entrypoints/options/__tests__/App.test.tsx index fb26771..d627a0c 100644 --- a/entrypoints/options/__tests__/App.test.tsx +++ b/entrypoints/options/__tests__/App.test.tsx @@ -21,6 +21,7 @@ vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key: string) => key, }), + withTranslation: () => (Component: any) => Component, })); describe('Options App', () => { diff --git a/pages/Base64Converter/__tests__/TextMode.test.tsx b/pages/Base64Converter/__tests__/TextMode.test.tsx index c5d86d3..7dc94bd 100644 --- a/pages/Base64Converter/__tests__/TextMode.test.tsx +++ b/pages/Base64Converter/__tests__/TextMode.test.tsx @@ -4,6 +4,7 @@ import TextMode from '../TextMode'; // Mock CopyButton vi.mock('@/components/CopyButton', () => ({ + CopyButton: ({ text }: { text: string }) => , default: ({ text }: { text: string }) => , })); @@ -39,7 +40,7 @@ describe('TextMode', () => { await waitFor(() => { expect(screen.getByText('base64Converter:base64Output')).toBeInTheDocument(); }); - expect(screen.getByTestId('copy-button')).toHaveTextContent('SGVsbG8='); + expect(screen.getByRole('button', { name: 'SGVsbG8=' })).toBeInTheDocument(); }); it('应该解码 Base64 文本', async () => { @@ -58,7 +59,7 @@ describe('TextMode', () => { await waitFor(() => { expect(screen.getByText('base64Converter:textOutput')).toBeInTheDocument(); }); - expect(screen.getByTestId('copy-button')).toHaveTextContent('Hello'); + expect(screen.getByRole('button', { name: 'Hello' })).toBeInTheDocument(); }); it('应该对无效 Base64 显示错误', async () => { @@ -91,7 +92,7 @@ describe('TextMode', () => { }); await waitFor(() => { - expect(screen.getByTestId('copy-button')).toHaveTextContent('SGVsbG8='); + expect(screen.getByRole('button', { name: 'SGVsbG8=' })).toBeInTheDocument(); }); // 切换方向 @@ -99,7 +100,7 @@ describe('TextMode', () => { // 输出应该被清除 await waitFor(() => { - expect(screen.queryByTestId('copy-button')).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'SGVsbG8=' })).not.toBeInTheDocument(); }); }); @@ -114,13 +115,13 @@ describe('TextMode', () => { }); await waitFor(() => { - expect(screen.getByTestId('copy-button')).toHaveTextContent('SGVsbG8='); + expect(screen.getByRole('button', { name: 'SGVsbG8=' })).toBeInTheDocument(); }); fireEvent.click(screen.getByRole('button', { name: 'textInputArea.clear' })); await waitFor(() => { - expect(screen.queryByTestId('copy-button')).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'SGVsbG8=' })).not.toBeInTheDocument(); expect(input).toHaveValue(''); }); }); diff --git a/vitest.setup.ts b/vitest.setup.ts index 5e13962..d20df79 100644 --- a/vitest.setup.ts +++ b/vitest.setup.ts @@ -1,5 +1,26 @@ import '@testing-library/jest-dom'; import { afterEach, beforeEach, vi } from 'vitest'; +import { readFileSync } from 'fs'; +import { resolve } from 'path'; +import React from 'react'; + +// 读取中文翻译文件用于 withTranslation mock +const zhCommon = JSON.parse( + readFileSync(resolve(__dirname, 'i18n/locales/zh/common.json'), 'utf-8'), +); + +// 支持嵌套 key 查找,如 "errorBoundary.title" → zhCommon.errorBoundary.title +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function nestedLookup(obj: Record, key: string): string | undefined { + const parts = key.split('.'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + let current: any = obj; + for (const part of parts) { + if (current == null || typeof current !== 'object') return undefined; + current = current[part]; + } + return typeof current === 'string' ? current : undefined; +} vi.mock('@/utils/useLazyTranslation', () => ({ useLazyTranslation: (ns?: string) => ({ @@ -21,12 +42,53 @@ vi.mock('react-i18next', () => ({ language: 'zh-CN', }, }), + withTranslation: (ns?: string) => { + const translations = ns === 'common' ? zhCommon : {}; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (Component: React.ComponentType) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const Wrapped = (props: any) => + React.createElement(Component, { + ...props, + t: (key: string) => nestedLookup(translations, key) || key, + i18n: { language: 'zh-CN', changeLanguage: vi.fn() }, + }); + Wrapped.displayName = `withTranslation(${Component.displayName || Component.name || 'Component'})`; + return Wrapped; + }; + }, initReactI18next: { type: '3rdParty', init: vi.fn().mockResolvedValue(undefined), }, })); +vi.mock('@/components/CopyButton', () => ({ + CopyButton: ({ + text, + tooltip, + onClick, + }: { + text: string; + tooltip?: string; + onClick?: (e: React.MouseEvent) => void; + }) => + React.createElement( + 'button', + { + 'aria-label': tooltip || 'copy', + type: 'button', + onClick: async (e: React.MouseEvent) => { + if (text) { + await navigator.clipboard.writeText(text); + } + onClick?.(e); + }, + }, + 'Copy', + ), +})); + const storageMock = { local: { get: vi.fn().mockImplementation(() => Promise.resolve({})),