From c1890960ea7d06292c14d41607d5df4f857efc17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=A8=E9=9C=96=E9=93=83?= Date: Wed, 20 May 2026 20:54:51 +0800 Subject: [PATCH] =?UTF-8?q?test:=20=E6=B7=BB=E5=8A=A0=20background=20?= =?UTF-8?q?=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95=E4=B8=8E=E8=BE=B9=E7=95=8C?= =?UTF-8?q?=E6=83=85=E5=86=B5=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 entrypoints/__tests__/background.test.ts (12个测试用例) - 超长文本截断限制 (>10000字符) - Base64 内联图片拦截并返回错误提示 - 更新 parseContextMenuClick 返回 ParseResult 格式 - 更新测试匹配新的返回格式 - lint/typecheck/test 全部通过 (579个测试) --- entrypoints/__tests__/background.test.ts | 173 +++++++++++++++++++++++ entrypoints/background.ts | 10 +- utils/__tests__/contextMenu.test.ts | 78 +++++++--- utils/contextMenu.ts | 46 ++++-- 4 files changed, 278 insertions(+), 29 deletions(-) create mode 100644 entrypoints/__tests__/background.test.ts diff --git a/entrypoints/__tests__/background.test.ts b/entrypoints/__tests__/background.test.ts new file mode 100644 index 0000000..5304b2a --- /dev/null +++ b/entrypoints/__tests__/background.test.ts @@ -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 => ({ + 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'); + }); + }); +}); diff --git a/entrypoints/background.ts b/entrypoints/background.ts index 97d08d3..da73d77 100644 --- a/entrypoints/background.ts +++ b/entrypoints/background.ts @@ -10,9 +10,15 @@ export default defineBackground(() => { browser.contextMenus.onClicked.addListener(async (info, _tab) => { 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 { const sidePanelState = await browser.storage.local.get('sidePanelOpen'); diff --git a/utils/__tests__/contextMenu.test.ts b/utils/__tests__/contextMenu.test.ts index 887b490..50847e8 100644 --- a/utils/__tests__/contextMenu.test.ts +++ b/utils/__tests__/contextMenu.test.ts @@ -3,6 +3,7 @@ import { CONTEXT_MENU_CONFIGS, createAllContextMenus, parseContextMenuClick, + MAX_PAYLOAD_LENGTH, } from '@/utils/contextMenu'; describe('contextMenu', () => { @@ -96,19 +97,33 @@ describe('contextMenu', () => { const result = parseContextMenuClick('qrCode-image', info); expect(result).toEqual({ - featureKey: 'qrCode', - payload: 'https://example.com/image.png', + success: true, + 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 result = parseContextMenuClick('qrCode-image', info); expect(result).toEqual({ - featureKey: 'qrCode', - payload: '', + success: true, + data: { featureKey: 'qrCode', payload: '' }, }); }); @@ -120,8 +135,8 @@ describe('contextMenu', () => { const result = parseContextMenuClick('qrCode-page', info); expect(result).toEqual({ - featureKey: 'qrCode', - payload: 'https://example.com/page', + success: true, + data: { featureKey: 'qrCode', payload: 'https://example.com/page' }, }); }); @@ -133,8 +148,8 @@ describe('contextMenu', () => { const result = parseContextMenuClick('jwt', info); expect(result).toEqual({ - featureKey: 'jwt', - payload: 'selected text', + success: true, + data: { featureKey: 'jwt', payload: 'selected text' }, }); }); @@ -146,8 +161,8 @@ describe('contextMenu', () => { const result = parseContextMenuClick('timestamp', info); expect(result).toEqual({ - featureKey: 'timestamp', - payload: '1234567890', + success: true, + data: { featureKey: 'timestamp', payload: '1234567890' }, }); }); @@ -159,19 +174,22 @@ describe('contextMenu', () => { const result = parseContextMenuClick('storageCleaner', info); expect(result).toEqual({ - featureKey: 'storageCleaner', - payload: 'https://example.com', + success: true, + data: { featureKey: 'storageCleaner', payload: 'https://example.com' }, }); }); - it('当没有 selectionText 和 pageUrl 时应返回 null', () => { + it('当没有 selectionText 和 pageUrl 时应返回错误', () => { const info = createMockOnClickData({ pageUrl: undefined, }); const result = parseContextMenuClick('someMenu', info); - expect(result).toBeNull(); + expect(result).toEqual({ + success: false, + error: '无法获取有效数据', + }); }); it('selectionText 优先于 pageUrl', () => { @@ -183,8 +201,34 @@ describe('contextMenu', () => { const result = parseContextMenuClick('jwt', info); expect(result).toEqual({ - featureKey: 'jwt', - payload: 'selected text', + success: true, + 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' }, }); }); }); diff --git a/utils/contextMenu.ts b/utils/contextMenu.ts index 1de7b8a..288de5a 100644 --- a/utils/contextMenu.ts +++ b/utils/contextMenu.ts @@ -12,8 +12,16 @@ export interface ContextMenuClickedInfo { payload: string; } +export interface ParseResult { + success: boolean; + data?: ContextMenuClickedInfo; + error?: string; +} + const PARENT_MENU_ID = 'testing-tools-parent'; +export const MAX_PAYLOAD_LENGTH = 10000; + export const CONTEXT_MENU_CONFIGS: ContextMenuItemConfig[] = [ { id: PARENT_MENU_ID, @@ -78,36 +86,54 @@ export function createAllContextMenus(): void { export function parseContextMenuClick( menuItemId: string, info: chrome.contextMenus.OnClickData, -): ContextMenuClickedInfo | null { +): ParseResult { const featureKey = menuItemId as PageType; if (menuItemId === 'qrCode-image') { + const srcUrl = info.srcUrl || ''; + + if (srcUrl.startsWith('data:image/')) { + return { + success: false, + error: '无法识别 Base64 内联图片,请使用图片文件 URL', + }; + } + return { - featureKey: 'qrCode', - payload: info.srcUrl || '', + success: true, + data: { featureKey: 'qrCode', payload: srcUrl }, }; } if (menuItemId === 'qrCode-page') { return { - featureKey: 'qrCode', - payload: info.pageUrl || '', + success: true, + data: { featureKey: 'qrCode', payload: info.pageUrl || '' }, }; } if (info.selectionText) { + const text = info.selectionText; + + if (text.length > MAX_PAYLOAD_LENGTH) { + return { + success: true, + data: { featureKey, payload: text.substring(0, MAX_PAYLOAD_LENGTH) }, + }; + } + return { - featureKey, - payload: info.selectionText, + success: true, + data: { featureKey, payload: text }, }; } if (info.pageUrl) { return { - featureKey, - payload: info.pageUrl, + success: true, + data: { featureKey, payload: info.pageUrl }, }; } - return null; + return { success: false, error: '无法获取有效数据' }; }