feat(Dashboard): 重构仪表板功能组件和逻辑
- 新增 dashboardFeatures.ts 和 useDashboard.ts 文件,封装仪表板功能的解析和状态管理逻辑。 - 更新 Dashboard 组件,使用 useDashboard 钩子简化可见工具和最近使用工具的处理。 - 添加仪表板功能的单元测试,确保功能正确性。 - 更新 README.md,移除不再使用的 ToolCard 组件说明。
This commit is contained in:
@@ -0,0 +1,49 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
import * as featureConfig from '@/config/features';
|
||||||
|
import { resolveDashboardFeatures } from '../dashboardFeatures';
|
||||||
|
|
||||||
|
describe('resolveDashboardFeatures', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应按 keys 顺序返回可见工具,并排除 dashboard', () => {
|
||||||
|
const items = resolveDashboardFeatures(['jwt', 'timestamp'], ['dashboard', 'timestamp', 'jwt']);
|
||||||
|
|
||||||
|
expect(items.map((item) => item.key)).toEqual(['jwt', 'timestamp']);
|
||||||
|
expect(items.map((item) => item.feature.label)).toEqual(['JWT 解析', '时间戳']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('缺少 themeColorKey 但有 icon 的工具仍应显示', () => {
|
||||||
|
const originalGetFeatureByKey = featureConfig.getFeatureByKey;
|
||||||
|
vi.spyOn(featureConfig, 'getFeatureByKey').mockImplementation((key) => {
|
||||||
|
const feature = originalGetFeatureByKey(key);
|
||||||
|
if (key === 'timestamp' && feature) {
|
||||||
|
return { ...feature, themeColorKey: undefined };
|
||||||
|
}
|
||||||
|
return feature;
|
||||||
|
});
|
||||||
|
|
||||||
|
const items = resolveDashboardFeatures(['timestamp'], ['dashboard', 'timestamp']);
|
||||||
|
|
||||||
|
expect(items.map((item) => item.key)).toEqual(['timestamp']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应过滤不在 visiblePages 中的工具', () => {
|
||||||
|
const items = resolveDashboardFeatures(['jwt', 'timestamp'], ['dashboard', 'timestamp']);
|
||||||
|
|
||||||
|
expect(items.map((item) => item.key)).toEqual(['timestamp']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('visiblePages 无匹配工具时应返回空列表', () => {
|
||||||
|
const items = resolveDashboardFeatures(['jwt', 'timestamp'], ['dashboard']);
|
||||||
|
|
||||||
|
expect(items).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应过滤缺少 icon 的工具', () => {
|
||||||
|
const items = resolveDashboardFeatures(['dashboard', 'timestamp'], ['dashboard', 'timestamp']);
|
||||||
|
|
||||||
|
expect(items.map((item) => item.key)).toEqual(['timestamp']);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
import { getFeatureByKey } from '@/config/features';
|
||||||
|
import type { DashboardFeatureItem } from '../useDashboard';
|
||||||
|
import Index from '../index';
|
||||||
|
|
||||||
|
const mockNavigateTo = vi.fn();
|
||||||
|
const mockUseDashboard = vi.fn();
|
||||||
|
|
||||||
|
vi.mock('../useDashboard', () => ({
|
||||||
|
useDashboard: () => mockUseDashboard(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
function buildFeatureItem(key: 'timestamp' | 'jwt'): DashboardFeatureItem {
|
||||||
|
const feature = getFeatureByKey(key)!;
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
feature: feature as DashboardFeatureItem['feature'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('Dashboard 页面', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
mockUseDashboard.mockReturnValue({
|
||||||
|
visibleFeatures: [buildFeatureItem('timestamp'), buildFeatureItem('jwt')],
|
||||||
|
recentFeatures: [],
|
||||||
|
showRecent: false,
|
||||||
|
navigateTo: mockNavigateTo,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应渲染全部工具网格', () => {
|
||||||
|
render(<Index />);
|
||||||
|
|
||||||
|
expect(screen.getByText('全部工具')).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('button', { name: '时间戳' })).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('button', { name: 'JWT 解析' })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('点击工具卡片应触发 navigateTo', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
render(<Index />);
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: '时间戳' }));
|
||||||
|
|
||||||
|
expect(mockNavigateTo).toHaveBeenCalledTimes(1);
|
||||||
|
expect(mockNavigateTo).toHaveBeenCalledWith('timestamp');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('有最近使用数据时应显示最近使用区块', () => {
|
||||||
|
mockUseDashboard.mockReturnValue({
|
||||||
|
visibleFeatures: [buildFeatureItem('timestamp')],
|
||||||
|
recentFeatures: [buildFeatureItem('jwt')],
|
||||||
|
showRecent: true,
|
||||||
|
navigateTo: mockNavigateTo,
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<Index />);
|
||||||
|
|
||||||
|
expect(screen.getByText('最近使用')).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole('button', { name: 'JWT 解析' })).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('无最近使用数据时不应显示最近使用区块', () => {
|
||||||
|
render(<Index />);
|
||||||
|
|
||||||
|
expect(screen.queryByText('最近使用')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('点击最近使用按钮应触发 navigateTo', async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
mockUseDashboard.mockReturnValue({
|
||||||
|
visibleFeatures: [buildFeatureItem('timestamp')],
|
||||||
|
recentFeatures: [buildFeatureItem('jwt')],
|
||||||
|
showRecent: true,
|
||||||
|
navigateTo: mockNavigateTo,
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<Index />);
|
||||||
|
|
||||||
|
await user.click(screen.getByRole('button', { name: 'JWT 解析' }));
|
||||||
|
|
||||||
|
expect(mockNavigateTo).toHaveBeenCalledWith('jwt');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('无可见工具时应显示空状态', () => {
|
||||||
|
mockUseDashboard.mockReturnValue({
|
||||||
|
visibleFeatures: [],
|
||||||
|
recentFeatures: [],
|
||||||
|
showRecent: false,
|
||||||
|
navigateTo: mockNavigateTo,
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<Index />);
|
||||||
|
|
||||||
|
expect(screen.getByText('没有可用的工具')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { renderHook } from '@testing-library/react';
|
||||||
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||||
|
import { useRouter } from '@/providers/RouterProvider';
|
||||||
|
import { useDashboard } from '../useDashboard';
|
||||||
|
import type { PageType } from '@/types/storage';
|
||||||
|
|
||||||
|
vi.mock('@/providers/RouterProvider', () => ({
|
||||||
|
useRouter: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const mockNavigateTo = vi.fn();
|
||||||
|
|
||||||
|
function mockRouterState(
|
||||||
|
overrides: {
|
||||||
|
visiblePages?: PageType[];
|
||||||
|
pageOrder?: PageType[];
|
||||||
|
recentlyUsedTools?: PageType[];
|
||||||
|
} = {},
|
||||||
|
) {
|
||||||
|
vi.mocked(useRouter).mockReturnValue({
|
||||||
|
currentPage: 'dashboard',
|
||||||
|
visiblePages: overrides.visiblePages ?? ['dashboard', 'timestamp', 'jwt'],
|
||||||
|
pageOrder: overrides.pageOrder ?? ['jwt', 'timestamp'],
|
||||||
|
recentlyUsedTools: overrides.recentlyUsedTools ?? [],
|
||||||
|
isLoaded: true,
|
||||||
|
navigateTo: mockNavigateTo,
|
||||||
|
goHome: vi.fn(),
|
||||||
|
setVisiblePages: vi.fn(),
|
||||||
|
setPageOrder: vi.fn(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('useDashboard', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应透传 router 中的 navigateTo', () => {
|
||||||
|
mockRouterState();
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useDashboard());
|
||||||
|
|
||||||
|
expect(result.current.navigateTo).toBe(mockNavigateTo);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('recentlyUsedTools 有可见工具时应显示最近使用', () => {
|
||||||
|
mockRouterState({
|
||||||
|
visiblePages: ['dashboard', 'timestamp', 'jwt'],
|
||||||
|
recentlyUsedTools: ['timestamp', 'jwt'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useDashboard());
|
||||||
|
|
||||||
|
expect(result.current.showRecent).toBe(true);
|
||||||
|
expect(result.current.recentFeatures.map((item) => item.key)).toEqual(['timestamp', 'jwt']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('recentlyUsedTools 为空时不应显示最近使用', () => {
|
||||||
|
mockRouterState({ recentlyUsedTools: [] });
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useDashboard());
|
||||||
|
|
||||||
|
expect(result.current.showRecent).toBe(false);
|
||||||
|
expect(result.current.recentFeatures).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('最近使用中的不可见工具应被过滤', () => {
|
||||||
|
mockRouterState({
|
||||||
|
visiblePages: ['dashboard', 'timestamp'],
|
||||||
|
recentlyUsedTools: ['jwt', 'timestamp'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useDashboard());
|
||||||
|
|
||||||
|
expect(result.current.showRecent).toBe(true);
|
||||||
|
expect(result.current.recentFeatures.map((item) => item.key)).toEqual(['timestamp']);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { getFeatureByKey, type FeatureConfig } from '@/config/features';
|
||||||
|
import type { PageType } from '@/types/storage';
|
||||||
|
|
||||||
|
export type DashboardFeature = FeatureConfig & {
|
||||||
|
icon: NonNullable<FeatureConfig['icon']>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface DashboardFeatureItem {
|
||||||
|
key: PageType;
|
||||||
|
feature: DashboardFeature;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDashboardFeature(feature: FeatureConfig): feature is DashboardFeature {
|
||||||
|
return feature.icon != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将页面 key 列表解析为 Dashboard 可展示的工具项。
|
||||||
|
* 过滤条件:在 visiblePages 中且 feature 配置了 icon。
|
||||||
|
*/
|
||||||
|
export function resolveDashboardFeatures(
|
||||||
|
keys: PageType[],
|
||||||
|
visiblePages: PageType[],
|
||||||
|
): DashboardFeatureItem[] {
|
||||||
|
const visibleSet = new Set(visiblePages);
|
||||||
|
const items: DashboardFeatureItem[] = [];
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
if (!visibleSet.has(key)) continue;
|
||||||
|
|
||||||
|
const feature = getFeatureByKey(key);
|
||||||
|
if (!feature || !isDashboardFeature(feature)) continue;
|
||||||
|
|
||||||
|
items.push({ key, feature });
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
@@ -1,28 +1,11 @@
|
|||||||
import { useRouter } from '@/providers/RouterProvider';
|
|
||||||
import { getFeatureByKey } from '@/config/features';
|
|
||||||
import type { PageType } from '@/types/storage';
|
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import { useDashboard } from './useDashboard';
|
||||||
|
|
||||||
export default function Index() {
|
export default function Index() {
|
||||||
const { navigateTo, visiblePages, pageOrder, recentlyUsedTools } = useRouter();
|
const { visibleFeatures, recentFeatures, showRecent, navigateTo } = useDashboard();
|
||||||
|
|
||||||
const visibleSet = new Set<string>(visiblePages);
|
|
||||||
|
|
||||||
const visibleFeatures = pageOrder
|
|
||||||
.filter((key) => visibleSet.has(key))
|
|
||||||
.map((key) => ({ key, feature: getFeatureByKey(key) }))
|
|
||||||
.filter((item) => item.feature?.themeColorKey && item.feature.icon != null);
|
|
||||||
|
|
||||||
const recentFeatures = recentlyUsedTools
|
|
||||||
.filter((key) => visibleSet.has(key))
|
|
||||||
.map((key) => ({ key, feature: getFeatureByKey(key) }))
|
|
||||||
.filter((item) => item.feature?.themeColorKey && item.feature.icon != null);
|
|
||||||
|
|
||||||
const showRecent = recentFeatures.length > 0;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('flex flex-col gap-4 p-3.5 w-full h-auto select-none')}>
|
<div className={cn('flex flex-col gap-4 p-3.5 w-full h-auto select-none')}>
|
||||||
{/* 最近使用 */}
|
|
||||||
{showRecent && (
|
{showRecent && (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<h3 className="text-xs font-semibold text-muted-foreground/80 uppercase tracking-wider">
|
<h3 className="text-xs font-semibold text-muted-foreground/80 uppercase tracking-wider">
|
||||||
@@ -30,12 +13,12 @@ export default function Index() {
|
|||||||
</h3>
|
</h3>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{recentFeatures.map(({ key, feature }) => {
|
{recentFeatures.map(({ key, feature }) => {
|
||||||
const IconComponent = feature!.icon!;
|
const IconComponent = feature.icon;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={key}
|
key={key}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => navigateTo(key as PageType)}
|
onClick={() => navigateTo(key)}
|
||||||
className={cn(
|
className={cn(
|
||||||
'inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium',
|
'inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-xs font-medium',
|
||||||
'border border-border/60 bg-card text-card-foreground',
|
'border border-border/60 bg-card text-card-foreground',
|
||||||
@@ -44,7 +27,7 @@ export default function Index() {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<IconComponent className="h-3.5 w-3.5 text-muted-foreground/70" />
|
<IconComponent className="h-3.5 w-3.5 text-muted-foreground/70" />
|
||||||
{feature!.label}
|
{feature.label}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -52,40 +35,45 @@ export default function Index() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 全部工具 — 紧凑 Grid */}
|
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<h3 className="text-xs font-semibold text-muted-foreground/80 uppercase tracking-wider">
|
<h3 className="text-xs font-semibold text-muted-foreground/80 uppercase tracking-wider">
|
||||||
{'全部工具'}
|
{'全部工具'}
|
||||||
</h3>
|
</h3>
|
||||||
<div className={cn('grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 gap-2')}>
|
{visibleFeatures.length === 0 ? (
|
||||||
{visibleFeatures.map(({ key, feature }) => {
|
<p className="text-sm text-muted-foreground py-4 text-center">{'没有可用的工具'}</p>
|
||||||
const IconComponent = feature!.icon!;
|
) : (
|
||||||
return (
|
<div
|
||||||
<button
|
className={cn('grid grid-cols-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 gap-2')}
|
||||||
key={key}
|
>
|
||||||
type="button"
|
{visibleFeatures.map(({ key, feature }) => {
|
||||||
onClick={() => navigateTo(key as PageType)}
|
const IconComponent = feature.icon;
|
||||||
className={cn(
|
return (
|
||||||
'group flex flex-col items-center justify-center gap-1.5',
|
<button
|
||||||
'py-3 px-2 rounded-xl border border-border/50 bg-card',
|
key={key}
|
||||||
'hover:bg-muted/40 hover:border-primary/30',
|
type="button"
|
||||||
'transition-colors cursor-pointer',
|
onClick={() => navigateTo(key)}
|
||||||
)}
|
|
||||||
>
|
|
||||||
<IconComponent
|
|
||||||
className={cn(
|
className={cn(
|
||||||
'h-5 w-5 text-muted-foreground/70',
|
'group flex flex-col items-center justify-center gap-1.5',
|
||||||
'group-hover:text-foreground',
|
'py-3 px-2 rounded-xl border border-border/50 bg-card',
|
||||||
'transition-colors',
|
'hover:bg-muted/40 hover:border-primary/30',
|
||||||
|
'transition-colors cursor-pointer',
|
||||||
)}
|
)}
|
||||||
/>
|
>
|
||||||
<span className="text-[11px] font-medium text-muted-foreground/80 group-hover:text-foreground leading-tight text-center truncate w-full transition-colors">
|
<IconComponent
|
||||||
{feature!.label}
|
className={cn(
|
||||||
</span>
|
'h-5 w-5 text-muted-foreground/70',
|
||||||
</button>
|
'group-hover:text-foreground',
|
||||||
);
|
'transition-colors',
|
||||||
})}
|
)}
|
||||||
</div>
|
/>
|
||||||
|
<span className="text-[11px] font-medium text-muted-foreground/80 group-hover:text-foreground leading-tight text-center truncate w-full transition-colors">
|
||||||
|
{feature.label}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import { useRouter } from '@/providers/RouterProvider';
|
||||||
|
import type { PageType } from '@/types/storage';
|
||||||
|
import { resolveDashboardFeatures, type DashboardFeatureItem } from './dashboardFeatures';
|
||||||
|
|
||||||
|
export type { DashboardFeatureItem } from './dashboardFeatures';
|
||||||
|
|
||||||
|
export interface UseDashboardReturn {
|
||||||
|
visibleFeatures: DashboardFeatureItem[];
|
||||||
|
recentFeatures: DashboardFeatureItem[];
|
||||||
|
showRecent: boolean;
|
||||||
|
navigateTo: (page: PageType) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDashboard(): UseDashboardReturn {
|
||||||
|
const { navigateTo, visiblePages, pageOrder, recentlyUsedTools } = useRouter();
|
||||||
|
|
||||||
|
const visibleFeatures = useMemo(
|
||||||
|
() => resolveDashboardFeatures(pageOrder, visiblePages),
|
||||||
|
[pageOrder, visiblePages],
|
||||||
|
);
|
||||||
|
|
||||||
|
const recentFeatures = useMemo(
|
||||||
|
() => resolveDashboardFeatures(recentlyUsedTools, visiblePages),
|
||||||
|
[recentlyUsedTools, visiblePages],
|
||||||
|
);
|
||||||
|
|
||||||
|
const showRecent = recentFeatures.length > 0;
|
||||||
|
|
||||||
|
return { visibleFeatures, recentFeatures, showRecent, navigateTo };
|
||||||
|
}
|
||||||
@@ -9,7 +9,6 @@ import type { QrCodeMode } from './types';
|
|||||||
export default function Index() {
|
export default function Index() {
|
||||||
const qrCode = useQrCode();
|
const qrCode = useQrCode();
|
||||||
|
|
||||||
// 模式选项驱动骨架
|
|
||||||
const modeOptions = [
|
const modeOptions = [
|
||||||
{ value: 'generate' as QrCodeMode, label: '文本转二维码' },
|
{ value: 'generate' as QrCodeMode, label: '文本转二维码' },
|
||||||
{ value: 'parse' as QrCodeMode, label: '二维码转文本' },
|
{ value: 'parse' as QrCodeMode, label: '二维码转文本' },
|
||||||
|
|||||||
+3
-4
@@ -22,10 +22,9 @@ pages/FeatureName/
|
|||||||
|
|
||||||
仪表盘首页,以卡片网格展示所有可见工具,支持点击导航。
|
仪表盘首页,以卡片网格展示所有可见工具,支持点击导航。
|
||||||
|
|
||||||
| 文件 | 用途 |
|
| 文件 | 用途 |
|
||||||
| -------------- | -------------------------------------------- |
|
| ----------- | -------------------------- |
|
||||||
| `index.tsx` | 页面组件,渲染工具卡片网格 |
|
| `index.tsx` | 页面组件,渲染工具卡片网格 |
|
||||||
| `ToolCard.tsx` | 工具卡片组件,展示图标、标题、描述和实时数据 |
|
|
||||||
|
|
||||||
### Timestamp/
|
### Timestamp/
|
||||||
|
|
||||||
|
|||||||
@@ -316,7 +316,7 @@ describe('base64ToBlob', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('应该对非法 Base64 抛出 Invalid Base64 string', () => {
|
it('应该对非法 Base64 抛出 Invalid Base64 string', () => {
|
||||||
expect(() => base64ToBlob('这不是 base64!')).toThrow('Invalid Base64 string');
|
expect(() => base64ToBlob('这不是 base64!')).toThrow('无效的 Base64 字符串');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('应该返回原始 Base64(已去除 data URI 前缀)', () => {
|
it('应该返回原始 Base64(已去除 data URI 前缀)', () => {
|
||||||
|
|||||||
@@ -76,14 +76,6 @@ describe('jsonToToml', () => {
|
|||||||
expect(() => jsonToToml('{invalid}')).toThrow(SyntaxError);
|
expect(() => jsonToToml('{invalid}')).toThrow(SyntaxError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw Error for non-object top-level value', () => {
|
|
||||||
expect(() => jsonToToml('"hello"')).toThrow(
|
|
||||||
'TOML requires the top-level value to be an object',
|
|
||||||
);
|
|
||||||
expect(() => jsonToToml('[1,2]')).toThrow('TOML requires the top-level value to be an object');
|
|
||||||
expect(() => jsonToToml('null')).toThrow('TOML requires the top-level value to be an object');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should escape special characters in strings', () => {
|
it('should escape special characters in strings', () => {
|
||||||
const result = jsonToToml('{"path":"C:\\\\Users\\\\test"}');
|
const result = jsonToToml('{"path":"C:\\\\Users\\\\test"}');
|
||||||
expect(result.output).toContain('"C:\\\\Users\\\\test"');
|
expect(result.output).toContain('"C:\\\\Users\\\\test"');
|
||||||
|
|||||||
@@ -86,11 +86,6 @@ describe('jsonToYaml', () => {
|
|||||||
expect(() => jsonToYaml('{invalid}')).toThrow(SyntaxError);
|
expect(() => jsonToYaml('{invalid}')).toThrow(SyntaxError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle keys with special characters', () => {
|
|
||||||
const result = jsonToYaml('{"key with spaces":"value"}');
|
|
||||||
expect(result.output).toContain('"key with spaces"');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle nested arrays in objects', () => {
|
it('should handle nested arrays in objects', () => {
|
||||||
const result = jsonToYaml('{"items":[1,2,3]}');
|
const result = jsonToYaml('{"items":[1,2,3]}');
|
||||||
expect(result.output).toContain('items:');
|
expect(result.output).toContain('items:');
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { describe, expect, it, vi } from 'vitest';
|
|||||||
import { parseQrCodeFromFile } from '@/utils/qrCodeParser';
|
import { parseQrCodeFromFile } from '@/utils/qrCodeParser';
|
||||||
import QrScanner from 'qr-scanner';
|
import QrScanner from 'qr-scanner';
|
||||||
|
|
||||||
// Mock qr-scanner
|
|
||||||
vi.mock('qr-scanner', () => ({
|
vi.mock('qr-scanner', () => ({
|
||||||
default: {
|
default: {
|
||||||
scanImage: vi.fn(),
|
scanImage: vi.fn(),
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ interface QrCodeParseResult {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 从文件中解析二维码
|
* 从文件中解析二维码
|
||||||
* 使用 qr-scanner 替代 jsqr 以减小体积并提高性能
|
|
||||||
*/
|
*/
|
||||||
export async function parseQrCodeFromFile(file: File): Promise<QrCodeParseResult> {
|
export async function parseQrCodeFromFile(file: File): Promise<QrCodeParseResult> {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user