Compare commits

..

2 Commits

Author SHA1 Message Date
rsgltzyd 6589ea3858 refactor: 移除侧边栏相关功能和代码 2026-07-05 21:47:35 +08:00
rsgltzyd 6be9b31ca2 docs: 将spec目录移动到docs目录中 2026-07-05 21:34:41 +08:00
21 changed files with 23 additions and 262 deletions
-11
View File
@@ -120,14 +120,3 @@ export function getAllFeatureKeys(): PageType[] {
export function getDefaultPageOrder(): PageType[] {
return FEATURES.filter((f) => f.key !== 'dashboard').map((f) => f.key);
}
export function getEntryPointType(): 'popup' | 'sidepanel' | 'tab' {
const pathname = window.location.pathname;
if (pathname.includes('sidepanel')) {
return 'sidepanel';
}
if (new URLSearchParams(window.location.search).get('mode') === 'tab') {
return 'tab';
}
return 'popup';
}
-1
View File
@@ -4,6 +4,5 @@ export {
getAllFeatureKeys,
getDefaultPageOrder,
getDefaultVisibleFeatureKeys,
getEntryPointType,
getFeatureByKey,
} from '@/config/featureMeta';
+1 -24
View File
@@ -1,6 +1,6 @@
import '../../.wxt/types/imports.d.ts';
import { browser } from 'wxt/browser';
import { MessageAction, onMessage, sendMessage } from '@/utils/messages';
import { MessageAction, onMessage } from '@/utils/messages';
import { createAllContextMenus, parseContextMenuClick } from '@/utils/contextMenu';
import { saveContextMenuData } from '@/utils/useContextMenuData';
import { mainWorldInjectionScript } from '@/utils/rightClickInjection';
@@ -24,18 +24,6 @@ export default defineBackground(() => {
const { featureKey, payload } = result.data;
try {
const sidePanelState = await browser.storage.local.get('sidePanelOpen');
const isSidePanelOpen = sidePanelState.sidePanelOpen === true;
if (isSidePanelOpen) {
await sendMessage(MessageAction.CONTEXT_MENU_CLICKED, { featureKey, payload });
return;
}
} catch (err) {
console.debug('[Context Menu] Side panel pipeline is not available:', err);
}
await saveContextMenuData({ featureKey, payload });
try {
@@ -45,17 +33,6 @@ export default defineBackground(() => {
}
});
// 扩展图标点击时打开侧边栏
browser.action.onClicked.addListener(async (tab) => {
if (tab.id) {
try {
await browser.sidePanel.open({ tabId: tab.id });
} catch (err) {
console.error('Failed to open side panel via extension action clicked:', err);
}
}
});
// 3. 注入主环境脚本(content script 无权访问 chrome.tabs/scripting,委托 background 执行)
onMessage(MessageAction.INJECT_MAIN_WORLD_SCRIPT, async (message) => {
const sender = message.sender as chrome.runtime.MessageSender | undefined;
+5 -20
View File
@@ -2,30 +2,15 @@ import RouterProvider from '@/providers/RouterProvider';
import TopBar from '@/layout/TopBar';
import RouterContainer from '@/components/RouterContainer';
import ErrorBoundary from '@/components/ErrorBoundary';
import { getEntryPointType } from '@/config/features';
export default function App() {
const entryType = getEntryPointType();
const routerConfig =
entryType === 'tab'
? {
syncKey: 'app/tabRoute' as const,
visiblePagesKey: 'app/tabVisiblePages' as const,
pageOrderKey: 'app/tabPageOrder' as const,
}
: {
syncKey: 'app/popupRoute' as const,
visiblePagesKey: 'app/popupVisiblePages' as const,
pageOrderKey: 'app/popupPageOrder' as const,
};
return (
<RouterProvider
syncKey={routerConfig.syncKey}
visiblePagesKey={routerConfig.visiblePagesKey}
pageOrderKey={routerConfig.pageOrderKey}
syncKey="app/popupRoute"
visiblePagesKey="app/popupVisiblePages"
pageOrderKey="app/popupPageOrder"
>
<div className="flex flex-col w-[400px] max-w-[400px] min-w-[400px] h-[600px] min-h-[600px] overflow-hidden bg-background sm:w-screen sm:max-w-none sm:min-w-0 sm:h-screen sm:min-h-0">
<div className="flex flex-col w-[400px] max-w-[400px] min-w-[400px] h-[600px] min-h-[600px] overflow-hidden bg-background">
<TopBar />
<ErrorBoundary>
<RouterContainer />
+1 -8
View File
@@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Testing Tools - 标签页</title>
<title>Testing Tools</title>
<meta name="manifest.type" content="browser_action" />
<script>
(function () {
@@ -33,13 +33,6 @@
width: 100%;
height: 100%;
}
/* If opened in a tab (mode=tab), reset the fixed size */
@media screen and (min-width: 600px) {
html, body {
width: 100%;
height: 100%;
}
}
</style>
</head>
<body>
-26
View File
@@ -1,26 +0,0 @@
import { useEffect } from 'react';
import RouterProvider from '@/providers/RouterProvider';
import TopBar from '@/layout/TopBar';
import RouterContainer from '@/components/RouterContainer';
import ErrorBoundary from '@/components/ErrorBoundary';
import { MessageAction, sendMessage } from '@/utils/messages';
export default function App() {
useEffect(() => {
sendMessage(MessageAction.SIDE_PANEL_STATE_CHANGED, { isOpen: true });
return () => {
sendMessage(MessageAction.SIDE_PANEL_STATE_CHANGED, { isOpen: false });
};
}, []);
return (
<RouterProvider defaultRoute="dashboard" syncKey="app/sidepanelRoute">
<div className="flex flex-col h-screen w-full overflow-hidden">
<TopBar />
<ErrorBoundary>
<RouterContainer />
</ErrorBoundary>
</div>
</RouterProvider>
);
}
-25
View File
@@ -1,25 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Testing Tools - Side Panel</title>
<script>
(function () {
try {
var raw = localStorage.getItem('snapshot/app/themeMode');
var mode = raw ? JSON.parse(raw) : 'system';
if (mode !== 'light' && mode !== 'dark' && mode !== 'system') mode = 'system';
var isDark =
mode === 'dark' ||
(mode === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);
if (isDark) document.documentElement.classList.add('dark');
} catch (_error) {}
})();
</script>
</head>
<body>
<div id="root"></div>
<script type="module" src="./main.tsx"></script>
</body>
</html>
-10
View File
@@ -1,10 +0,0 @@
import ReactDOM from 'react-dom/client';
import AppRoot from '@/providers/AppRoot';
import '@/styles/shell.css';
import App from './App.tsx';
ReactDOM.createRoot(document.getElementById('root')!).render(
<AppRoot>
<App />
</AppRoot>,
);
+2 -11
View File
@@ -1,4 +1,4 @@
import { ArrowLeft, ExternalLink } from 'lucide-react';
import { ArrowLeft } from 'lucide-react';
import { Button } from '@/components/ui/button';
import SearchDropdown from './SearchDropdown';
import SearchInput from './SearchInput';
@@ -22,20 +22,11 @@ export default function TopBar() {
handleSelectFeature,
handleKeyDown,
cycleThemeMode,
handleOpenInTab,
goHome,
clearSearch,
} = useTopBar();
const actions = [
{ id: 'theme', icon: ThemeIcon, title: themeTitle, onClick: cycleThemeMode },
{
id: 'open-in-tab',
icon: ExternalLink,
title: '在标签页打开',
onClick: () => void handleOpenInTab(),
},
];
const actions = [{ id: 'theme', icon: ThemeIcon, title: themeTitle, onClick: cycleThemeMode }];
return (
<header className="relative z-50 flex h-14 items-center justify-between border-b border-border bg-background px-4">
-8
View File
@@ -11,7 +11,6 @@ import { useRouter } from '@/providers/RouterProvider';
import { useThemeMode } from '@/providers/ThemeModeProvider';
import { type FeatureConfig, FEATURES } from '@/config/features';
import { useStorageState } from '@/utils/useStorageState';
import { openExtensionPage } from '@/utils/chromeTabs';
import { isSearchHistory, SEARCH_HISTORY_DISPLAY, SEARCH_HISTORY_LIMIT } from './constants';
export interface UseTopBarReturn {
@@ -30,7 +29,6 @@ export interface UseTopBarReturn {
handleSelectFeature: (feature: FeatureConfig) => void;
handleKeyDown: (e: ReactKeyboardEvent) => void;
cycleThemeMode: () => void;
handleOpenInTab: () => Promise<void>;
goHome: () => void;
clearSearch: () => void;
}
@@ -51,11 +49,6 @@ export function useTopBar(): UseTopBarReturn {
const containerRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const handleOpenInTab = async () => {
await openExtensionPage('popup.html', { mode: 'tab' });
window.close();
};
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
@@ -175,7 +168,6 @@ export function useTopBar(): UseTopBarReturn {
handleSelectFeature,
handleKeyDown,
cycleThemeMode,
handleOpenInTab,
goHome,
clearSearch,
};
@@ -2,15 +2,6 @@ import { describe, expect, it, vi } from 'vitest';
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
import Base64ConverterPage from '../index';
// Mock getEntryPointType
vi.mock('@/config/features', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/config/features')>();
return {
...actual,
getEntryPointType: () => 'sidepanel',
};
});
// Mock 子组件
vi.mock('../components/TextMode', () => ({
default: ({ onSwitchToImageMode }: { onSwitchToImageMode?: () => void }) => (
@@ -18,14 +18,6 @@ vi.mock('@/components/GlobalSnackbar', () => ({
}),
}));
vi.mock('@/config/features', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/config/features')>();
return {
...actual,
getEntryPointType: () => 'sidepanel',
};
});
vi.mock('../components/QrCodePreview', () => ({
default: () => <div data-testid="qr-code-preview">QrCodePreview</div>,
}));
@@ -82,10 +82,10 @@ describe('RouterProvider', () => {
});
it('应该从指定的 syncKey 加载路由', async () => {
mockStorageBatch({ 'app/sidepanelRoute': 'timestamp' });
mockStorageBatch({ 'app/popupRoute': 'timestamp' });
render(
<RouterProvider syncKey="app/sidepanelRoute">
<RouterProvider syncKey="app/popupRoute">
<TestComponent />
</RouterProvider>,
);
@@ -118,31 +118,14 @@ describe('RouterProvider', () => {
expect(storageUtil.set).toHaveBeenCalledWith('app/popupRoute', 'timestamp');
});
it('应该支持独立的标签页路由同步', async () => {
mockStorageBatch({ 'app/tabRoute': 'qrCode' });
render(
<RouterProvider syncKey="app/tabRoute">
<TestComponent />
</RouterProvider>,
);
await waitFor(() => {
expect(screen.getByTestId('current-page')).toHaveTextContent('qrCode');
});
});
it('应该独立支持 visiblePagesKey 和 pageOrderKey,并合并缺失的新功能', async () => {
mockStorageBatch({
'app/sidepanelVisiblePages': ['timestamp', 'storageCleaner'],
'app/sidepanelPageOrder': ['storageCleaner', 'timestamp'],
'app/popupVisiblePages': ['timestamp', 'storageCleaner'],
'app/popupPageOrder': ['storageCleaner', 'timestamp'],
});
render(
<RouterProvider
visiblePagesKey="app/sidepanelVisiblePages"
pageOrderKey="app/sidepanelPageOrder"
>
<RouterProvider visiblePagesKey="app/popupVisiblePages" pageOrderKey="app/popupPageOrder">
<TestComponent />
</RouterProvider>,
);
+1 -13
View File
@@ -66,14 +66,10 @@ export interface FormMapEntry {
* 定义了所有持久化在客户端的数据结构
*/
export interface StorageSchema {
/** RouterProvider 的默认路由键,仅当未显式传入 syncKey 时使用(popup/sidepanel/tab 入口已各自覆盖) */
/** RouterProvider 的默认路由键,仅当未显式传入 syncKey 时使用(popup 入口已覆盖) */
'app/currentRoute': PageType;
/** Popup 窗口的当前路由 */
'app/popupRoute': PageType;
/** 侧边栏的当前路由 */
'app/sidepanelRoute': PageType;
/** 标签页的当前路由 */
'app/tabRoute': PageType;
/** RouterProvider 的默认可见页面列表键,仅当未显式传入 visiblePagesKey 时使用 */
'app/visiblePages': PageType[];
/** RouterProvider 的默认页面排序键,仅当未显式传入 pageOrderKey 时使用 */
@@ -82,14 +78,6 @@ export interface StorageSchema {
'app/popupVisiblePages': PageType[];
/** Popup 窗口页面的显示顺序 */
'app/popupPageOrder': PageType[];
/** 侧边栏可见的页面列表 */
'app/sidepanelVisiblePages': PageType[];
/** 侧边栏页面的显示顺序 */
'app/sidepanelPageOrder': PageType[];
/** 标签页可见的页面列表 */
'app/tabVisiblePages': PageType[];
/** 标签页页面的显示顺序 */
'app/tabPageOrder': PageType[];
/** 应用主题配置 */
'app/theme': string;
/** 主题模式偏好(light/dark/system */
-25
View File
@@ -1,25 +0,0 @@
import { describe, expect, it, vi } from 'vitest';
import { openExtensionPage } from '@/utils/chromeTabs';
describe('chromeTabs', () => {
describe('openExtensionPage', () => {
it('应该在新标签页中打开扩展页面', async () => {
await openExtensionPage('popup.html');
expect(chrome.runtime.getURL).toHaveBeenCalledWith('popup.html');
expect(chrome.tabs.create).toHaveBeenCalledWith({
url: 'chrome-extension://test-extension-id/popup.html',
});
});
it('当创建标签页失败时应记录错误', async () => {
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
(chrome.tabs.create as any).mockRejectedValue(new Error('Tab creation failed'));
await openExtensionPage('popup.html');
expect(consoleSpy).toHaveBeenCalledWith('打开扩展页面失败:', expect.any(Error));
consoleSpy.mockRestore();
});
});
});
+7 -4
View File
@@ -44,18 +44,21 @@ describe('messages', () => {
);
});
it('应该支持带数据的消息发送', async () => {
it('应该支持带数据的消息发送', async () => {
const mockSendMessage = await getMockSendMessage();
mockSendMessage.mockResolvedValue(undefined);
const mockTab = { id: 456, url: 'https://example.com' };
(chrome.tabs.query as any).mockResolvedValue([mockTab]);
await sendMessageToContent(MessageAction.SIDE_PANEL_STATE_CHANGED, { isOpen: true });
await sendMessageToContent(MessageAction.CONTEXT_MENU_CLICKED, {
featureKey: 'timestamp',
payload: '1234567890',
});
expect(mockSendMessage).toHaveBeenCalledWith(
MessageAction.SIDE_PANEL_STATE_CHANGED,
{ isOpen: true },
MessageAction.CONTEXT_MENU_CLICKED,
{ featureKey: 'timestamp', payload: '1234567890' },
456,
);
});
-23
View File
@@ -1,23 +0,0 @@
/**
* Chrome 标签页相关工具函数
*/
/**
* 在新标签页中打开扩展页面
* @param page - 扩展页面路径(如 'popup.html'
* @param params - 可选的查询参数
*/
export async function openExtensionPage(
page: string,
params?: Record<string, string>,
): Promise<void> {
try {
const url = new URL(chrome.runtime.getURL(page));
if (params) {
Object.entries(params).forEach(([key, value]) => url.searchParams.set(key, value));
}
await chrome.tabs.create({ url: url.toString() });
} catch (error) {
console.error('打开扩展页面失败:', error);
}
}
-2
View File
@@ -1,7 +1,6 @@
import { defineExtensionMessaging } from '@webext-core/messaging';
export enum MessageAction {
SIDE_PANEL_STATE_CHANGED = 'sidePanelStateChanged',
CONTEXT_MENU_CLICKED = 'contextMenuClicked',
RESTORE_RIGHT_CLICK = 'restoreRightClick',
QUERY_RIGHT_CLICK_STATUS = 'queryRightClickStatus',
@@ -20,7 +19,6 @@ export interface ContextMenuClickedPayload {
}
export interface ProtocolMap {
[MessageAction.SIDE_PANEL_STATE_CHANGED](data: { isOpen: boolean }): void;
[MessageAction.CONTEXT_MENU_CLICKED](data: ContextMenuClickedPayload): void;
[MessageAction.RESTORE_RIGHT_CLICK](data: undefined): MessageResponse & { restored: boolean };
[MessageAction.QUERY_RIGHT_CLICK_STATUS](
+1 -12
View File
@@ -41,15 +41,7 @@ export default defineConfig({
name: 'Testing Tool',
description: 'A tool for testing web applications.',
version_name: undefined,
permissions: [
'storage',
'clipboardWrite',
'scripting',
'tabs',
'cookies',
'sidePanel',
'contextMenus',
],
permissions: ['storage', 'clipboardWrite', 'scripting', 'tabs', 'cookies', 'contextMenus'],
host_permissions: ['<all_urls>'],
icons: {
'16': 'icon/16.png',
@@ -66,9 +58,6 @@ export default defineConfig({
'48': 'icon/48.png',
},
},
side_panel: {
default_path: 'entrypoints/sidepanel/index.html',
},
},
vite: () => ({
plugins: [manualChunksForHtmlOnly()],