Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6589ea3858 | |||
| 6be9b31ca2 |
@@ -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';
|
||||
}
|
||||
|
||||
@@ -4,6 +4,5 @@ export {
|
||||
getAllFeatureKeys,
|
||||
getDefaultPageOrder,
|
||||
getDefaultVisibleFeatureKeys,
|
||||
getEntryPointType,
|
||||
getFeatureByKey,
|
||||
} from '@/config/featureMeta';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>,
|
||||
);
|
||||
@@ -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">
|
||||
|
||||
@@ -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>,
|
||||
);
|
||||
|
||||
Vendored
+1
-13
@@ -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) */
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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()],
|
||||
|
||||
Reference in New Issue
Block a user