Compare commits

...

3 Commits

Author SHA1 Message Date
雨霖铃 64e23994a6 fix(test): 修复 CopyButton mock 未捕获 clipboard writeText 异常 2026-05-28 00:39:47 +08:00
雨霖铃 528621f16e fix(tests): 修复 CI 失败的测试 - 补充 withTranslation 和 CopyButton mock
- vitest.setup.ts: react-i18next mock 添加 withTranslation HOC(读取 zh/common.json 翻译)
- vitest.setup.ts: 添加 @/components/CopyButton 全局 mock
- App 测试: 补充 withTranslation 导出
- TextMode 测试: 修复多个 CopyButton 实例导致的 getByTestId 冲突,改用 getByRole
2026-05-28 00:13:09 +08:00
LingandRX 42d3fedbaa develop → main: 合并开发分支 (#51)
* fix(ci): fix release workflow artifact upload issue

- Split artifact upload into 3 separate steps (Chrome, Firefox, Source)
- Update download steps to match new artifact names
- Add if-no-files-found: error for better error handling
- Fix Node.js 20 compatibility issue with upload-artifact@v4 glob pattern

* docs: 更新 AGENTS.md,添加测试工具和翻译文件结构说明

* docs: 更新 AGENTS.md,完善 CI 步骤和存储键名格式说明

* fix: remove unnecessary animations from StorageCleaner page

- Remove entrance animations (animate-in, fade-in, zoom-in) from all components
- Remove scale effect (active:scale-[0.99]) from clean button
- Remove transition effects (transition-all, transition-colors) from OptionItem, StorageOptionsGrid, AutoRefreshToggle
- Remove bouncing animation from error icon
- Remove pulsing animation from warning banner
- Keep animate-spin on button loading spinner as functional indicator

* fix: remove unnecessary animations from RightClickRestorer page

* fix: remove all unnecessary animations from all pages

- Remove dead code animations (animate-in, fade-in, slide-in, zoom-in, shake) from tailwindcss-animate plugin (not installed)
- Remove decorative transition effects (transition-all, transition-colors) from all page components
- Remove scale effects (active:scale-95) from buttons
- Remove bounce animations (animate-bounce) from icons
- Keep functional animate-spin on loading spinners as they provide essential loading feedback

Affected pages: Dashboard, Timestamp, Jwt, JsonTools, Base64Converter, HtmlToMarkdown, MarkdownToHtml, QrCode, TextStatistics

---------

Co-authored-by: Ubuntu <ubuntu@localhost.localdomain>
2026-05-26 15:08:53 +08:00
3 changed files with 74 additions and 6 deletions
@@ -21,6 +21,7 @@ vi.mock('react-i18next', () => ({
useTranslation: () => ({ useTranslation: () => ({
t: (key: string) => key, t: (key: string) => key,
}), }),
withTranslation: () => (Component: any) => Component,
})); }));
describe('Options App', () => { describe('Options App', () => {
@@ -4,6 +4,7 @@ import TextMode from '../TextMode';
// Mock CopyButton // Mock CopyButton
vi.mock('@/components/CopyButton', () => ({ vi.mock('@/components/CopyButton', () => ({
CopyButton: ({ text }: { text: string }) => <button data-testid="copy-button">{text}</button>,
default: ({ text }: { text: string }) => <button data-testid="copy-button">{text}</button>, default: ({ text }: { text: string }) => <button data-testid="copy-button">{text}</button>,
})); }));
@@ -39,7 +40,7 @@ describe('TextMode', () => {
await waitFor(() => { await waitFor(() => {
expect(screen.getByText('base64Converter:base64Output')).toBeInTheDocument(); expect(screen.getByText('base64Converter:base64Output')).toBeInTheDocument();
}); });
expect(screen.getByTestId('copy-button')).toHaveTextContent('SGVsbG8='); expect(screen.getByRole('button', { name: 'SGVsbG8=' })).toBeInTheDocument();
}); });
it('应该解码 Base64 文本', async () => { it('应该解码 Base64 文本', async () => {
@@ -58,7 +59,7 @@ describe('TextMode', () => {
await waitFor(() => { await waitFor(() => {
expect(screen.getByText('base64Converter:textOutput')).toBeInTheDocument(); expect(screen.getByText('base64Converter:textOutput')).toBeInTheDocument();
}); });
expect(screen.getByTestId('copy-button')).toHaveTextContent('Hello'); expect(screen.getByRole('button', { name: 'Hello' })).toBeInTheDocument();
}); });
it('应该对无效 Base64 显示错误', async () => { it('应该对无效 Base64 显示错误', async () => {
@@ -91,7 +92,7 @@ describe('TextMode', () => {
}); });
await waitFor(() => { await waitFor(() => {
expect(screen.getByTestId('copy-button')).toHaveTextContent('SGVsbG8='); expect(screen.getByRole('button', { name: 'SGVsbG8=' })).toBeInTheDocument();
}); });
// 切换方向 // 切换方向
@@ -99,7 +100,7 @@ describe('TextMode', () => {
// 输出应该被清除 // 输出应该被清除
await waitFor(() => { 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(() => { await waitFor(() => {
expect(screen.getByTestId('copy-button')).toHaveTextContent('SGVsbG8='); expect(screen.getByRole('button', { name: 'SGVsbG8=' })).toBeInTheDocument();
}); });
fireEvent.click(screen.getByRole('button', { name: 'textInputArea.clear' })); fireEvent.click(screen.getByRole('button', { name: 'textInputArea.clear' }));
await waitFor(() => { await waitFor(() => {
expect(screen.queryByTestId('copy-button')).not.toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'SGVsbG8=' })).not.toBeInTheDocument();
expect(input).toHaveValue(''); expect(input).toHaveValue('');
}); });
}); });
+66
View File
@@ -1,5 +1,26 @@
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import { afterEach, beforeEach, vi } from 'vitest'; 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<string, any>, 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', () => ({ vi.mock('@/utils/useLazyTranslation', () => ({
useLazyTranslation: (ns?: string) => ({ useLazyTranslation: (ns?: string) => ({
@@ -21,12 +42,57 @@ vi.mock('react-i18next', () => ({
language: 'zh-CN', language: 'zh-CN',
}, },
}), }),
withTranslation: (ns?: string) => {
const translations = ns === 'common' ? zhCommon : {};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return (Component: React.ComponentType<any>) => {
// 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: { initReactI18next: {
type: '3rdParty', type: '3rdParty',
init: vi.fn().mockResolvedValue(undefined), 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) {
try {
await navigator.clipboard.writeText(text);
} catch {
// 与真实 CopyButton 行为一致:失败时静默处理
}
}
onClick?.(e);
},
},
'Copy',
),
}));
const storageMock = { const storageMock = {
local: { local: {
get: vi.fn().mockImplementation(() => Promise.resolve({})), get: vi.fn().mockImplementation(() => Promise.resolve({})),