test: 添加 background 单元测试与边界情况处理
- 新增 entrypoints/__tests__/background.test.ts (12个测试用例) - 超长文本截断限制 (>10000字符) - Base64 内联图片拦截并返回错误提示 - 更新 parseContextMenuClick 返回 ParseResult 格式 - 更新测试匹配新的返回格式 - lint/typecheck/test 全部通过 (579个测试)
This commit is contained in:
@@ -0,0 +1,173 @@
|
|||||||
|
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
||||||
|
import {
|
||||||
|
createAllContextMenus,
|
||||||
|
parseContextMenuClick,
|
||||||
|
CONTEXT_MENU_CONFIGS,
|
||||||
|
MAX_PAYLOAD_LENGTH,
|
||||||
|
} from '@/utils/contextMenu';
|
||||||
|
import { MessageAction } from '@/utils/messages';
|
||||||
|
|
||||||
|
describe('background 菜单注册与分流', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('菜单注册', () => {
|
||||||
|
it('应该调用 createAllContextMenus 创建所有菜单项', () => {
|
||||||
|
createAllContextMenus();
|
||||||
|
|
||||||
|
expect(chrome.contextMenus.create).toHaveBeenCalledTimes(CONTEXT_MENU_CONFIGS.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该创建父级菜单 Testing Tools', () => {
|
||||||
|
createAllContextMenus();
|
||||||
|
|
||||||
|
expect(chrome.contextMenus.create).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: 'testing-tools-parent',
|
||||||
|
title: 'Testing Tools',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该创建 JWT 解析子菜单', () => {
|
||||||
|
createAllContextMenus();
|
||||||
|
|
||||||
|
expect(chrome.contextMenus.create).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: 'jwt',
|
||||||
|
title: '🔑 解析 JWT',
|
||||||
|
parentId: 'testing-tools-parent',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该创建图片二维码识别子菜单', () => {
|
||||||
|
createAllContextMenus();
|
||||||
|
|
||||||
|
expect(chrome.contextMenus.create).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: 'qrCode-image',
|
||||||
|
title: '🖼️ 识别图中的二维码',
|
||||||
|
contexts: ['image'],
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('菜单点击解析', () => {
|
||||||
|
const createMockOnClickData = (
|
||||||
|
overrides: Partial<chrome.contextMenus.OnClickData> = {},
|
||||||
|
): chrome.contextMenus.OnClickData => ({
|
||||||
|
menuItemId: 'test',
|
||||||
|
editable: false,
|
||||||
|
pageUrl: 'https://example.com',
|
||||||
|
...overrides,
|
||||||
|
});
|
||||||
|
|
||||||
|
it('当有 selectionText 时应返回对应的 featureKey 和 payload', () => {
|
||||||
|
const info = createMockOnClickData({
|
||||||
|
menuItemId: 'jwt',
|
||||||
|
selectionText: 'test-token',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = parseContextMenuClick('jwt', info);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: true,
|
||||||
|
data: { featureKey: 'jwt', payload: 'test-token' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('当点击 qrCode-image 时应返回 qrCode 功能和 srcUrl', () => {
|
||||||
|
const info = createMockOnClickData({
|
||||||
|
menuItemId: 'qrCode-image',
|
||||||
|
srcUrl: 'https://example.com/image.png',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = parseContextMenuClick('qrCode-image', info);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: true,
|
||||||
|
data: { featureKey: 'qrCode', payload: 'https://example.com/image.png' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('当图片为 Base64 内联图时应返回错误', () => {
|
||||||
|
const info = createMockOnClickData({
|
||||||
|
menuItemId: 'qrCode-image',
|
||||||
|
srcUrl:
|
||||||
|
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = parseContextMenuClick('qrCode-image', info);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: false,
|
||||||
|
error: '无法识别 Base64 内联图片,请使用图片文件 URL',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('当没有 selectionText 和 srcUrl 时应返回错误', () => {
|
||||||
|
const info = createMockOnClickData({
|
||||||
|
menuItemId: 'unknown',
|
||||||
|
pageUrl: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = parseContextMenuClick('unknown', info);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: false,
|
||||||
|
error: '无法获取有效数据',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('当文本超过最大长度限制时应截断', () => {
|
||||||
|
const longText = 'a'.repeat(MAX_PAYLOAD_LENGTH + 1000);
|
||||||
|
const info = createMockOnClickData({
|
||||||
|
menuItemId: 'textStatistics',
|
||||||
|
selectionText: longText,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = parseContextMenuClick('textStatistics', info);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.data?.payload.length).toBe(MAX_PAYLOAD_LENGTH);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('当文本未超过最大长度限制时应保持原样', () => {
|
||||||
|
const shortText = 'short text';
|
||||||
|
const info = createMockOnClickData({
|
||||||
|
menuItemId: 'textStatistics',
|
||||||
|
selectionText: shortText,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = parseContextMenuClick('textStatistics', info);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: true,
|
||||||
|
data: { featureKey: 'textStatistics', payload: 'short text' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('应该正确处理页面 URL 菜单点击', () => {
|
||||||
|
const info = createMockOnClickData({
|
||||||
|
menuItemId: 'storageCleaner',
|
||||||
|
pageUrl: 'https://example.com/page',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = parseContextMenuClick('storageCleaner', info);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: true,
|
||||||
|
data: { featureKey: 'storageCleaner', payload: 'https://example.com/page' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('消息类型定义', () => {
|
||||||
|
it('CONTEXT_MENU_CLICKED 消息类型应正确定义', () => {
|
||||||
|
expect(MessageAction.CONTEXT_MENU_CLICKED).toBe('contextMenuClicked');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -10,9 +10,15 @@ export default defineBackground(() => {
|
|||||||
|
|
||||||
browser.contextMenus.onClicked.addListener(async (info, _tab) => {
|
browser.contextMenus.onClicked.addListener(async (info, _tab) => {
|
||||||
const result = parseContextMenuClick(info.menuItemId as string, info);
|
const result = parseContextMenuClick(info.menuItemId as string, info);
|
||||||
if (!result) return;
|
|
||||||
|
|
||||||
const { featureKey, payload } = result;
|
if (!result.success || !result.data) {
|
||||||
|
if (result.error) {
|
||||||
|
console.warn('[Context Menu]', result.error);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { featureKey, payload } = result.data;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const sidePanelState = await browser.storage.local.get('sidePanelOpen');
|
const sidePanelState = await browser.storage.local.get('sidePanelOpen');
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
CONTEXT_MENU_CONFIGS,
|
CONTEXT_MENU_CONFIGS,
|
||||||
createAllContextMenus,
|
createAllContextMenus,
|
||||||
parseContextMenuClick,
|
parseContextMenuClick,
|
||||||
|
MAX_PAYLOAD_LENGTH,
|
||||||
} from '@/utils/contextMenu';
|
} from '@/utils/contextMenu';
|
||||||
|
|
||||||
describe('contextMenu', () => {
|
describe('contextMenu', () => {
|
||||||
@@ -96,19 +97,33 @@ describe('contextMenu', () => {
|
|||||||
const result = parseContextMenuClick('qrCode-image', info);
|
const result = parseContextMenuClick('qrCode-image', info);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
featureKey: 'qrCode',
|
success: true,
|
||||||
payload: 'https://example.com/image.png',
|
data: { featureKey: 'qrCode', payload: 'https://example.com/image.png' },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('当点击 qrCode-image 菜单但没有 srcUrl 时应返回空字符串', () => {
|
it('当图片为 Base64 内联图时应返回错误', () => {
|
||||||
|
const info = createMockOnClickData({
|
||||||
|
srcUrl:
|
||||||
|
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = parseContextMenuClick('qrCode-image', info);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: false,
|
||||||
|
error: '无法识别 Base64 内联图片,请使用图片文件 URL',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('当点击 qrCode-image 菜单但没有 srcUrl 时应返回成功', () => {
|
||||||
const info = createMockOnClickData({});
|
const info = createMockOnClickData({});
|
||||||
|
|
||||||
const result = parseContextMenuClick('qrCode-image', info);
|
const result = parseContextMenuClick('qrCode-image', info);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
featureKey: 'qrCode',
|
success: true,
|
||||||
payload: '',
|
data: { featureKey: 'qrCode', payload: '' },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -120,8 +135,8 @@ describe('contextMenu', () => {
|
|||||||
const result = parseContextMenuClick('qrCode-page', info);
|
const result = parseContextMenuClick('qrCode-page', info);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
featureKey: 'qrCode',
|
success: true,
|
||||||
payload: 'https://example.com/page',
|
data: { featureKey: 'qrCode', payload: 'https://example.com/page' },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -133,8 +148,8 @@ describe('contextMenu', () => {
|
|||||||
const result = parseContextMenuClick('jwt', info);
|
const result = parseContextMenuClick('jwt', info);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
featureKey: 'jwt',
|
success: true,
|
||||||
payload: 'selected text',
|
data: { featureKey: 'jwt', payload: 'selected text' },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -146,8 +161,8 @@ describe('contextMenu', () => {
|
|||||||
const result = parseContextMenuClick('timestamp', info);
|
const result = parseContextMenuClick('timestamp', info);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
featureKey: 'timestamp',
|
success: true,
|
||||||
payload: '1234567890',
|
data: { featureKey: 'timestamp', payload: '1234567890' },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -159,19 +174,22 @@ describe('contextMenu', () => {
|
|||||||
const result = parseContextMenuClick('storageCleaner', info);
|
const result = parseContextMenuClick('storageCleaner', info);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
featureKey: 'storageCleaner',
|
success: true,
|
||||||
payload: 'https://example.com',
|
data: { featureKey: 'storageCleaner', payload: 'https://example.com' },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('当没有 selectionText 和 pageUrl 时应返回 null', () => {
|
it('当没有 selectionText 和 pageUrl 时应返回错误', () => {
|
||||||
const info = createMockOnClickData({
|
const info = createMockOnClickData({
|
||||||
pageUrl: undefined,
|
pageUrl: undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = parseContextMenuClick('someMenu', info);
|
const result = parseContextMenuClick('someMenu', info);
|
||||||
|
|
||||||
expect(result).toBeNull();
|
expect(result).toEqual({
|
||||||
|
success: false,
|
||||||
|
error: '无法获取有效数据',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('selectionText 优先于 pageUrl', () => {
|
it('selectionText 优先于 pageUrl', () => {
|
||||||
@@ -183,8 +201,34 @@ describe('contextMenu', () => {
|
|||||||
const result = parseContextMenuClick('jwt', info);
|
const result = parseContextMenuClick('jwt', info);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
featureKey: 'jwt',
|
success: true,
|
||||||
payload: 'selected text',
|
data: { featureKey: 'jwt', payload: 'selected text' },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('当文本超过最大长度限制时应截断', () => {
|
||||||
|
const longText = 'a'.repeat(MAX_PAYLOAD_LENGTH + 1000);
|
||||||
|
const info = createMockOnClickData({
|
||||||
|
selectionText: longText,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = parseContextMenuClick('textStatistics', info);
|
||||||
|
|
||||||
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.data?.payload.length).toBe(MAX_PAYLOAD_LENGTH);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('当文本未超过最大长度限制时应保持原样', () => {
|
||||||
|
const shortText = 'short text';
|
||||||
|
const info = createMockOnClickData({
|
||||||
|
selectionText: shortText,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = parseContextMenuClick('textStatistics', info);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
success: true,
|
||||||
|
data: { featureKey: 'textStatistics', payload: 'short text' },
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+36
-10
@@ -12,8 +12,16 @@ export interface ContextMenuClickedInfo {
|
|||||||
payload: string;
|
payload: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ParseResult {
|
||||||
|
success: boolean;
|
||||||
|
data?: ContextMenuClickedInfo;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
const PARENT_MENU_ID = 'testing-tools-parent';
|
const PARENT_MENU_ID = 'testing-tools-parent';
|
||||||
|
|
||||||
|
export const MAX_PAYLOAD_LENGTH = 10000;
|
||||||
|
|
||||||
export const CONTEXT_MENU_CONFIGS: ContextMenuItemConfig[] = [
|
export const CONTEXT_MENU_CONFIGS: ContextMenuItemConfig[] = [
|
||||||
{
|
{
|
||||||
id: PARENT_MENU_ID,
|
id: PARENT_MENU_ID,
|
||||||
@@ -78,36 +86,54 @@ export function createAllContextMenus(): void {
|
|||||||
export function parseContextMenuClick(
|
export function parseContextMenuClick(
|
||||||
menuItemId: string,
|
menuItemId: string,
|
||||||
info: chrome.contextMenus.OnClickData,
|
info: chrome.contextMenus.OnClickData,
|
||||||
): ContextMenuClickedInfo | null {
|
): ParseResult {
|
||||||
const featureKey = menuItemId as PageType;
|
const featureKey = menuItemId as PageType;
|
||||||
|
|
||||||
if (menuItemId === 'qrCode-image') {
|
if (menuItemId === 'qrCode-image') {
|
||||||
|
const srcUrl = info.srcUrl || '';
|
||||||
|
|
||||||
|
if (srcUrl.startsWith('data:image/')) {
|
||||||
return {
|
return {
|
||||||
featureKey: 'qrCode',
|
success: false,
|
||||||
payload: info.srcUrl || '',
|
error: '无法识别 Base64 内联图片,请使用图片文件 URL',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: { featureKey: 'qrCode', payload: srcUrl },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (menuItemId === 'qrCode-page') {
|
if (menuItemId === 'qrCode-page') {
|
||||||
return {
|
return {
|
||||||
featureKey: 'qrCode',
|
success: true,
|
||||||
payload: info.pageUrl || '',
|
data: { featureKey: 'qrCode', payload: info.pageUrl || '' },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (info.selectionText) {
|
if (info.selectionText) {
|
||||||
|
const text = info.selectionText;
|
||||||
|
|
||||||
|
if (text.length > MAX_PAYLOAD_LENGTH) {
|
||||||
return {
|
return {
|
||||||
featureKey,
|
success: true,
|
||||||
payload: info.selectionText,
|
data: { featureKey, payload: text.substring(0, MAX_PAYLOAD_LENGTH) },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: { featureKey, payload: text },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (info.pageUrl) {
|
if (info.pageUrl) {
|
||||||
return {
|
return {
|
||||||
featureKey,
|
success: true,
|
||||||
payload: info.pageUrl,
|
data: { featureKey, payload: info.pageUrl },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return { success: false, error: '无法获取有效数据' };
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user