Files
testing-tool/vitest.setup.ts
T
LingandRX d4c29a1bf4 Develop (#54)
* 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

* docs: 添加项目文档(copilot-instructions、编码规范、各目录 README)

- 新增 .github/copilot-instructions.md:Copilot 指令文件,涵盖构建命令、CI 流水线、项目架构、关键规范
- 新增 .github/CODING_STANDARDS.md:完整的代码编写规范文档(TypeScript、React、样式、错误处理、命名、测试、Hook、i18n、存储等 12 个章节)
- 新增 11 个目录的 README.md:components、components/ui、config、entrypoints、i18n、lib、pages、providers、public、src、types、utils
- 更新 AGENTS.md:补充页面组件模式说明和 components/ui 目录

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(CopyButton): use shadcn buttonVariants instead of duplicated variant classes

Remove hand-written variantClasses/sizeClasses that duplicated shadcn's
buttonVariants. Now extends ButtonProps and imports buttonVariants from
components/ui/button for consistent styling.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(ErrorBoundary): use shadcn destructive tokens instead of hardcoded red colors

Replace hardcoded red-200/red-50/red-600 with border-destructive,
bg-destructive/5, text-destructive. Use variant='destructive' on
Button. Align error log styling with PageErrorBoundary.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(TextInputArea): replace hardcoded Chinese strings with i18n t() calls

Use existing translation keys for clear success, copy success, and
copy error messages instead of hardcoded Chinese text.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(QrCodePreview): use shadcn Button instead of manual button styles

Replace hand-written button class strings with shadcn Button component
using outline and default variants. Removes ~15 lines of duplicated
Tailwind classes.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs(GlobalSnackbar): clarify relationship with sonner toast

Add note explaining when to use GlobalSnackbar (provider context,
severity levels, custom positioning) vs sonner toast (simple one-off
messages).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(ImageUploader): wrap handleClearFile with useCallback

Prevents unnecessary re-renders by memoizing the callback, consistent
with handleFileChange which already uses useCallback.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(SwitchButtonGroup): remove redundant containerPadding conditional

Both branches of the ternary returned 'p-1'. Inline the constant
directly into the className string.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(TopBar): add missing openExtensionPage import

The function was called in handleOpenInTab but never imported from
utils/chromeTabs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(TopBar): add selectedIndex boundary protection in keyboard nav

Guard against out-of-bounds access when search results change during
keyboard navigation. Check selectedIndex < totalItems before accessing
arrays.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(RouterContainer): remove unnecessary empty-dep useMemo

getEntryPointType() is a simple config getter. Replace useMemo(() => ..., [])
with a direct call — the empty dep array made the memo pointless.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* feat(i18n): add missing translation keys for error boundaries and copy messages

Add keys for errorBoundary, pageErrorBoundary, router, and
messages.copyEmpty in both zh and en locales.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(CopyButton): replace hardcoded Chinese strings with i18n t() calls

Use useTranslation('common') for tooltip, copy empty/success/error
toast messages.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(ErrorBoundary): replace hardcoded Chinese with i18n via withTranslation HOC

Use react-i18next withTranslation HOC for class component to translate
title, description, and refresh button text.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(PageErrorBoundary): replace hardcoded Chinese with i18n via withTranslation HOC

Use react-i18next withTranslation HOC for class component to translate
title, description, and retry button text.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(RouterContainer): replace hardcoded Chinese with i18n t() calls

Use useTranslation('common') for 404 page title and description with
entryPointType interpolation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(pages): 修复 TypeScript 错误、硬编码字符串,移除死代码

- 修复 LiveClock.tsx CopyButton size 属性类型错误 (small → sm)
- 修复 ToolCard.tsx 紫色 RGB 值拼写错误 (147,51,2 purple → 147,51,232)
- 替换 4 处硬编码中文为 i18n 调用 (StorageCleaner, JsonTools)
- MarkdownToHtml 预览链接色改为 CSS 变量以支持暗黑模式
- 移除 Base64Converter 中已被 Base64ConverterSection 替代的死代码
  (FileMode.tsx, ImageMode.tsx 及其测试文件)
- 更新 README.md 移除对已删除文件的引用

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(CopyButton, LiveClock): 修改按钮大小为 'icon',移除硬编码颜色配置

* refactor(TextInputArea): 复用 CopyButton 组件替换内联复制逻辑

* fix(test): 修复 CopyButton mock 未捕获 clipboard writeText 异常

---------

Co-authored-by: Ubuntu <ubuntu@localhost.localdomain>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-28 00:40:54 +08:00

216 lines
6.1 KiB
TypeScript

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<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', () => ({
useLazyTranslation: (ns?: string) => ({
// 自动承接命名空间前缀过滤,100% 模拟真实多语种直出
t: (key: string) => (ns ? `${ns}:${key}` : key),
i18n: {
changeLanguage: vi.fn().mockResolvedValue(undefined),
language: 'zh-CN',
},
isLoaded: true,
}),
}));
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
i18n: {
changeLanguage: vi.fn().mockResolvedValue(undefined),
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: {
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) {
try {
await navigator.clipboard.writeText(text);
} catch {
// 与真实 CopyButton 行为一致:失败时静默处理
}
}
onClick?.(e);
},
},
'Copy',
),
}));
const storageMock = {
local: {
get: vi.fn().mockImplementation(() => Promise.resolve({})),
set: vi.fn().mockImplementation(() => Promise.resolve()),
remove: vi.fn().mockImplementation(() => Promise.resolve()),
clear: vi.fn().mockImplementation(() => Promise.resolve()),
},
session: {
get: vi.fn().mockImplementation(() => Promise.resolve({})),
set: vi.fn().mockImplementation(() => Promise.resolve()),
remove: vi.fn().mockImplementation(() => Promise.resolve()),
clear: vi.fn().mockImplementation(() => Promise.resolve()),
},
onChanged: {
addListener: vi.fn(),
removeListener: vi.fn(),
hasListener: vi.fn(),
},
};
const webExtensionMock = {
storage: storageMock,
tabs: {
query: vi.fn().mockResolvedValue([]),
get: vi.fn().mockResolvedValue({}),
sendMessage: vi.fn().mockResolvedValue(undefined),
create: vi.fn().mockResolvedValue({}),
reload: vi.fn().mockResolvedValue(undefined), // ✅ 承接 MessageAction.RELOAD_TAB 刷新单元测试
},
runtime: {
id: 'test-extension-id',
getURL: vi.fn((path: string) => `chrome-extension://test-extension-id/${path}`),
openOptionsPage: vi.fn(),
onInstalled: {
addListener: vi.fn(),
removeListener: vi.fn(),
},
},
action: {
onClicked: {
addListener: vi.fn(),
removeListener: vi.fn(),
},
openPopup: vi.fn().mockResolvedValue(undefined),
},
sidePanel: {
open: vi.fn().mockResolvedValue(undefined),
},
scripting: {
executeScript: vi.fn().mockResolvedValue([{ result: undefined }]),
},
cookies: {
getAll: vi.fn().mockResolvedValue([]),
remove: vi.fn().mockResolvedValue(undefined),
},
alarms: {
create: vi.fn().mockResolvedValue(undefined),
onAlarm: {
addListener: vi.fn(),
removeListener: vi.fn(),
},
},
contextMenus: {
create: vi.fn(),
onClicked: {
addListener: vi.fn(),
removeListener: vi.fn(),
},
ContextType: {
ALL: 'all',
PAGE: 'page',
FRAME: 'frame',
SELECTION: 'selection',
LINK: 'link',
EDITABLE: 'editable',
IMAGE: 'image',
VIDEO: 'video',
AUDIO: 'audio',
LAUNCHER: 'launcher',
BROWSER_ACTION: 'browser_action',
PAGE_ACTION: 'page_action',
ACTION: 'action',
},
},
};
Object.defineProperty(globalThis, 'chrome', { value: webExtensionMock, writable: true });
Object.defineProperty(globalThis, 'browser', { value: webExtensionMock, writable: true });
if (typeof window !== 'undefined') {
Object.defineProperty(window, 'chrome', { value: webExtensionMock, writable: true });
Object.defineProperty(window, 'browser', { value: webExtensionMock, writable: true });
}
beforeEach(() => {
vi.clearAllMocks();
storageMock.local.get.mockResolvedValue({});
storageMock.session.get.mockResolvedValue({});
});
afterEach(() => {
vi.restoreAllMocks();
});
// 全局 matchMedia 极客级环境模拟(ThemeModeProvider 依赖)
beforeEach(() => {
Object.defineProperty(window, 'matchMedia', {
writable: true,
value: vi.fn().mockImplementation((query: string) => ({
matches: false,
media: query,
onchange: null,
addListener: vi.fn(),
removeListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
dispatchEvent: vi.fn(),
})),
});
});