refactor(storageCleaner): simplify architecture and improve type semantics
- Remove RELOAD_TAB message chain, use chrome.tabs.reload() directly - Rename IndexedDB label to '站点存储' for accuracy - Introduce StorageSizeInfo type to distinguish bytes vs count - Rename totalSize to totalBytes for clarity - Merge runCleanScript into runScript to reduce duplication - Rename CleaningResult.success to overallSuccess to avoid confusion - Remove unused domain state and setDomain call Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -697,7 +697,7 @@
|
|||||||
"description": "Translation key: storageCleaner_options_sessionStorage"
|
"description": "Translation key: storageCleaner_options_sessionStorage"
|
||||||
},
|
},
|
||||||
"storageCleaner_options_indexedDB": {
|
"storageCleaner_options_indexedDB": {
|
||||||
"message": "IndexedDB",
|
"message": "站点存储",
|
||||||
"description": "Translation key: storageCleaner_options_indexedDB"
|
"description": "Translation key: storageCleaner_options_indexedDB"
|
||||||
},
|
},
|
||||||
"storageCleaner_options_cookies": {
|
"storageCleaner_options_cookies": {
|
||||||
|
|||||||
@@ -83,22 +83,4 @@ export default defineBackground(() => {
|
|||||||
return { success: false, message: errorMsg };
|
return { success: false, message: errorMsg };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
onMessage(MessageAction.RELOAD_TAB, async (message) => {
|
|
||||||
const { tabId, delay = 0 } = message.data;
|
|
||||||
|
|
||||||
const executeReload = () => {
|
|
||||||
browser.tabs.reload(tabId).catch((err) => {
|
|
||||||
console.error('Failed to execute tab reload operation:', err);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
if (delay > 0) {
|
|
||||||
setTimeout(executeReload, delay);
|
|
||||||
} else {
|
|
||||||
executeReload();
|
|
||||||
}
|
|
||||||
|
|
||||||
return { success: true };
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export default function CleaningResult({ result, className, ...props }: Cleaning
|
|||||||
|
|
||||||
if (!result) return null;
|
if (!result) return null;
|
||||||
|
|
||||||
const isSuccess = result.success;
|
const isSuccess = result.overallSuccess;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn('w-full', className)} {...props}>
|
<div className={cn('w-full', className)} {...props}>
|
||||||
|
|||||||
@@ -2,28 +2,29 @@ import React from 'react';
|
|||||||
import { formatBytes } from '@/utils/format';
|
import { formatBytes } from '@/utils/format';
|
||||||
import { useI18n } from '@/utils/chromeI18n';
|
import { useI18n } from '@/utils/chromeI18n';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
// 引入官方的 Checkbox 原子组件
|
import type { StorageSizeInfo } from './useStorageCleaner';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
|
|
||||||
interface OptionItemProps extends React.HTMLAttributes<HTMLDivElement> {
|
interface OptionItemProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
labelKey: string;
|
labelKey: string;
|
||||||
checked: boolean;
|
checked: boolean;
|
||||||
size?: number;
|
sizeInfo?: StorageSizeInfo;
|
||||||
isCount?: boolean;
|
|
||||||
onChange: () => void;
|
onChange: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function OptionItem({
|
export default function OptionItem({
|
||||||
labelKey,
|
labelKey,
|
||||||
checked,
|
checked,
|
||||||
size,
|
sizeInfo,
|
||||||
isCount = false,
|
|
||||||
onChange,
|
onChange,
|
||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: OptionItemProps) {
|
}: OptionItemProps) {
|
||||||
const { t } = useI18n('storageCleaner');
|
const { t } = useI18n('storageCleaner');
|
||||||
|
|
||||||
|
const sizeValue = sizeInfo?.value;
|
||||||
|
const isCount = sizeInfo?.displayType === 'count';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
onClick={onChange}
|
onClick={onChange}
|
||||||
@@ -46,14 +47,14 @@ export default function OptionItem({
|
|||||||
{t(labelKey)}
|
{t(labelKey)}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
{size !== undefined && size > 0 ? (
|
{sizeValue !== undefined && sizeValue > 0 ? (
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
'block text-[10px] font-mono font-medium mt-0.5 tabular-nums transition-colors',
|
'block text-[10px] font-mono font-medium mt-0.5 tabular-nums transition-colors',
|
||||||
checked ? 'text-primary/70' : 'text-muted-foreground/70',
|
checked ? 'text-primary/70' : 'text-muted-foreground/70',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isCount ? `${size} ${t('storageCleaner:countUnit')}` : formatBytes(size)}
|
{isCount ? `${sizeValue} ${t('storageCleaner:countUnit')}` : formatBytes(sizeValue)}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="block text-[10px] font-medium text-muted-foreground/50 mt-0.5 italic">
|
<span className="block text-[10px] font-medium text-muted-foreground/50 mt-0.5 italic">
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type { StorageCleanerOptions } from '@/types/storage';
|
import type { StorageCleanerOptions } from '@/types/storage';
|
||||||
|
import type { StorageSizeInfo } from './useStorageCleaner';
|
||||||
import OptionItem from './OptionItem';
|
import OptionItem from './OptionItem';
|
||||||
import { useI18n } from '@/utils/chromeI18n';
|
import { useI18n } from '@/utils/chromeI18n';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
@@ -8,7 +9,7 @@ import { Label } from '@/components/ui/label';
|
|||||||
|
|
||||||
interface StorageOptionsGridProps extends React.HTMLAttributes<HTMLDivElement> {
|
interface StorageOptionsGridProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
options: StorageCleanerOptions;
|
options: StorageCleanerOptions;
|
||||||
sizes: Record<string, number>;
|
sizes: Record<string, StorageSizeInfo>;
|
||||||
allSelected: boolean;
|
allSelected: boolean;
|
||||||
someSelected: boolean;
|
someSelected: boolean;
|
||||||
onOptionChange: (key: keyof StorageCleanerOptions) => void;
|
onOptionChange: (key: keyof StorageCleanerOptions) => void;
|
||||||
@@ -27,13 +28,13 @@ export default function StorageOptionsGrid({
|
|||||||
}: StorageOptionsGridProps) {
|
}: StorageOptionsGridProps) {
|
||||||
const { t } = useI18n('storageCleaner');
|
const { t } = useI18n('storageCleaner');
|
||||||
|
|
||||||
const optionKeys: { key: keyof StorageCleanerOptions; isCount?: boolean }[] = [
|
const optionKeys: (keyof StorageCleanerOptions)[] = [
|
||||||
{ key: 'localStorage' },
|
'localStorage',
|
||||||
{ key: 'sessionStorage' },
|
'sessionStorage',
|
||||||
{ key: 'indexedDB' },
|
'indexedDB',
|
||||||
{ key: 'cookies' },
|
'cookies',
|
||||||
{ key: 'cacheStorage', isCount: true },
|
'cacheStorage',
|
||||||
{ key: 'serviceWorkers', isCount: true },
|
'serviceWorkers',
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleToggleAll = () => {
|
const handleToggleAll = () => {
|
||||||
@@ -44,13 +45,12 @@ export default function StorageOptionsGrid({
|
|||||||
<div className={cn('w-full overflow-hidden', className)} {...props}>
|
<div className={cn('w-full overflow-hidden', className)} {...props}>
|
||||||
<div className="px-3.5 pt-3.5 pb-2">
|
<div className="px-3.5 pt-3.5 pb-2">
|
||||||
<div className="grid grid-cols-2 gap-2 items-stretch">
|
<div className="grid grid-cols-2 gap-2 items-stretch">
|
||||||
{optionKeys.map(({ key, isCount }) => (
|
{optionKeys.map((key) => (
|
||||||
<OptionItem
|
<OptionItem
|
||||||
key={key}
|
key={key}
|
||||||
labelKey={`storageCleaner:options.${key}`}
|
labelKey={`storageCleaner:options.${key}`}
|
||||||
checked={options[key]}
|
checked={options[key]}
|
||||||
size={sizes[key]}
|
sizeInfo={sizes[key]}
|
||||||
isCount={isCount}
|
|
||||||
onChange={() => onOptionChange(key)}
|
onChange={() => onOptionChange(key)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import {
|
|||||||
getSessionStorageSize,
|
getSessionStorageSize,
|
||||||
isRestrictedUrl,
|
isRestrictedUrl,
|
||||||
} from '@/utils/storageCleaner';
|
} from '@/utils/storageCleaner';
|
||||||
import { MessageAction, sendMessage } from '@/utils/messages';
|
|
||||||
import { useI18n } from '@/utils/chromeI18n';
|
import { useI18n } from '@/utils/chromeI18n';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
@@ -34,18 +33,22 @@ const DEFAULT_PREFERENCES: StorageCleanerPreferences = {
|
|||||||
selectedTypes: DEFAULT_OPTIONS,
|
selectedTypes: DEFAULT_OPTIONS,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface StorageSizeInfo {
|
||||||
|
value: number;
|
||||||
|
displayType: 'bytes' | 'count';
|
||||||
|
}
|
||||||
|
|
||||||
export interface UseStorageCleanerReturn {
|
export interface UseStorageCleanerReturn {
|
||||||
domain: string;
|
|
||||||
error: string;
|
error: string;
|
||||||
isInitializing: boolean;
|
isInitializing: boolean;
|
||||||
options: StorageCleanerOptions;
|
options: StorageCleanerOptions;
|
||||||
sizes: Record<string, number>;
|
sizes: Record<string, StorageSizeInfo>;
|
||||||
reloadAfterClean: boolean;
|
reloadAfterClean: boolean;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
result: CleaningResult | null;
|
result: CleaningResult | null;
|
||||||
showConfirm: boolean;
|
showConfirm: boolean;
|
||||||
setShowConfirm: (show: boolean) => void;
|
setShowConfirm: (show: boolean) => void;
|
||||||
totalSize: number;
|
totalBytes: number;
|
||||||
allSelected: boolean;
|
allSelected: boolean;
|
||||||
someSelected: boolean;
|
someSelected: boolean;
|
||||||
|
|
||||||
@@ -57,11 +60,10 @@ export interface UseStorageCleanerReturn {
|
|||||||
|
|
||||||
export function useStorageCleaner(): UseStorageCleanerReturn {
|
export function useStorageCleaner(): UseStorageCleanerReturn {
|
||||||
const { t } = useI18n(['storageCleaner', 'common']);
|
const { t } = useI18n(['storageCleaner', 'common']);
|
||||||
const [domain, setDomain] = useState<string>('');
|
|
||||||
const [error, setError] = useState<string>('');
|
const [error, setError] = useState<string>('');
|
||||||
const [isInitializing, setIsInitializing] = useState<boolean>(true);
|
const [isInitializing, setIsInitializing] = useState<boolean>(true);
|
||||||
const [options, setOptions] = useState<StorageCleanerOptions>(DEFAULT_OPTIONS);
|
const [options, setOptions] = useState<StorageCleanerOptions>(DEFAULT_OPTIONS);
|
||||||
const [sizes, setSizes] = useState<Record<string, number>>({});
|
const [sizes, setSizes] = useState<Record<string, StorageSizeInfo>>({});
|
||||||
const [reloadAfterClean, setReloadAfterClean] = useState<boolean>(true);
|
const [reloadAfterClean, setReloadAfterClean] = useState<boolean>(true);
|
||||||
const [loading, setLoading] = useState<boolean>(false);
|
const [loading, setLoading] = useState<boolean>(false);
|
||||||
const [result, setResult] = useState<CleaningResult | null>(null);
|
const [result, setResult] = useState<CleaningResult | null>(null);
|
||||||
@@ -102,7 +104,6 @@ export function useStorageCleaner(): UseStorageCleanerReturn {
|
|||||||
setError('');
|
setError('');
|
||||||
const url = tab.url;
|
const url = tab.url;
|
||||||
const tabId = tab.id!;
|
const tabId = tab.id!;
|
||||||
setDomain(new URL(url).hostname);
|
|
||||||
|
|
||||||
const [savedPrefs, cSize, lsSize, ssSize, idbSize, cacheCount, swCount] = await Promise.all([
|
const [savedPrefs, cSize, lsSize, ssSize, idbSize, cacheCount, swCount] = await Promise.all([
|
||||||
storageUtil.get('storageCleaner/preferences', DEFAULT_PREFERENCES),
|
storageUtil.get('storageCleaner/preferences', DEFAULT_PREFERENCES),
|
||||||
@@ -122,12 +123,12 @@ export function useStorageCleaner(): UseStorageCleanerReturn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setSizes({
|
setSizes({
|
||||||
cookies: cSize,
|
cookies: { value: cSize, displayType: 'bytes' },
|
||||||
localStorage: lsSize,
|
localStorage: { value: lsSize, displayType: 'bytes' },
|
||||||
sessionStorage: ssSize,
|
sessionStorage: { value: ssSize, displayType: 'bytes' },
|
||||||
indexedDB: idbSize,
|
indexedDB: { value: idbSize, displayType: 'bytes' },
|
||||||
cacheStorage: cacheCount,
|
cacheStorage: { value: cacheCount, displayType: 'count' },
|
||||||
serviceWorkers: swCount,
|
serviceWorkers: { value: swCount, displayType: 'count' },
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
if (currentRequestId === requestIdRef.current) {
|
if (currentRequestId === requestIdRef.current) {
|
||||||
@@ -217,9 +218,9 @@ export function useStorageCleaner(): UseStorageCleanerReturn {
|
|||||||
const cleaningResult = await clearStorage(tab.id, tab.url, options);
|
const cleaningResult = await clearStorage(tab.id, tab.url, options);
|
||||||
setResult(cleaningResult);
|
setResult(cleaningResult);
|
||||||
|
|
||||||
if (reloadAfterClean && cleaningResult.success) {
|
if (reloadAfterClean && cleaningResult.overallSuccess) {
|
||||||
toast.success(t('storageCleaner:cleanSuccessReload'));
|
toast.success(t('storageCleaner:cleanSuccessReload'));
|
||||||
await sendMessage(MessageAction.RELOAD_TAB, { tabId: tab.id, delay: 1000 });
|
await chrome.tabs.reload(tab.id);
|
||||||
} else {
|
} else {
|
||||||
await loadInfo();
|
await loadInfo();
|
||||||
}
|
}
|
||||||
@@ -231,13 +232,10 @@ export function useStorageCleaner(): UseStorageCleanerReturn {
|
|||||||
}
|
}
|
||||||
}, [options, reloadAfterClean, loadInfo, t]);
|
}, [options, reloadAfterClean, loadInfo, t]);
|
||||||
|
|
||||||
const totalSize = useMemo(() => {
|
const totalBytes = useMemo(() => {
|
||||||
return (
|
return Object.values(sizes).reduce((acc, s) => {
|
||||||
(sizes.cookies || 0) +
|
return s.displayType === 'bytes' ? acc + (s.value || 0) : acc;
|
||||||
(sizes.localStorage || 0) +
|
}, 0);
|
||||||
(sizes.sessionStorage || 0) +
|
|
||||||
(sizes.indexedDB || 0)
|
|
||||||
);
|
|
||||||
}, [sizes]);
|
}, [sizes]);
|
||||||
|
|
||||||
const selectionMetrics = useMemo(() => {
|
const selectionMetrics = useMemo(() => {
|
||||||
@@ -248,7 +246,6 @@ export function useStorageCleaner(): UseStorageCleanerReturn {
|
|||||||
}, [options]);
|
}, [options]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
domain,
|
|
||||||
error,
|
error,
|
||||||
isInitializing,
|
isInitializing,
|
||||||
options,
|
options,
|
||||||
@@ -258,7 +255,7 @@ export function useStorageCleaner(): UseStorageCleanerReturn {
|
|||||||
result,
|
result,
|
||||||
showConfirm,
|
showConfirm,
|
||||||
setShowConfirm,
|
setShowConfirm,
|
||||||
totalSize,
|
totalBytes,
|
||||||
allSelected: selectionMetrics.all,
|
allSelected: selectionMetrics.all,
|
||||||
someSelected: selectionMetrics.some,
|
someSelected: selectionMetrics.some,
|
||||||
handleReloadAfterCleanChange,
|
handleReloadAfterCleanChange,
|
||||||
|
|||||||
Vendored
+1
-1
@@ -189,7 +189,7 @@ export type StorageCleanResult =
|
|||||||
*/
|
*/
|
||||||
export interface CleaningResult {
|
export interface CleaningResult {
|
||||||
/** 整体操作是否成功 */
|
/** 整体操作是否成功 */
|
||||||
success: boolean;
|
overallSuccess: boolean;
|
||||||
/** 整体错误信息(如果有) */
|
/** 整体错误信息(如果有) */
|
||||||
error?: string;
|
error?: string;
|
||||||
/** 各项清理的具体结果 */
|
/** 各项清理的具体结果 */
|
||||||
|
|||||||
@@ -34,10 +34,14 @@ describe('messages', () => {
|
|||||||
const mockTab = { id: 123, url: 'https://example.com' };
|
const mockTab = { id: 123, url: 'https://example.com' };
|
||||||
(chrome.tabs.query as any).mockResolvedValue([mockTab]);
|
(chrome.tabs.query as any).mockResolvedValue([mockTab]);
|
||||||
|
|
||||||
const result = await sendMessageToContent(MessageAction.RELOAD_TAB, { tabId: 123 });
|
const result = await sendMessageToContent(MessageAction.RESTORE_RIGHT_CLICK);
|
||||||
|
|
||||||
expect(result).toEqual(mockResponse);
|
expect(result).toEqual(mockResponse);
|
||||||
expect(mockSendMessage).toHaveBeenCalledWith(MessageAction.RELOAD_TAB, { tabId: 123 }, 123);
|
expect(mockSendMessage).toHaveBeenCalledWith(
|
||||||
|
MessageAction.RESTORE_RIGHT_CLICK,
|
||||||
|
undefined,
|
||||||
|
123,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('应该支持不带数据的消息发送', async () => {
|
it('应该支持不带数据的消息发送', async () => {
|
||||||
@@ -60,11 +64,11 @@ describe('messages', () => {
|
|||||||
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||||
(chrome.tabs.query as any).mockResolvedValue([]);
|
(chrome.tabs.query as any).mockResolvedValue([]);
|
||||||
|
|
||||||
const result = await sendMessageToContent(MessageAction.RELOAD_TAB, { tabId: 123 });
|
const result = await sendMessageToContent(MessageAction.RESTORE_RIGHT_CLICK);
|
||||||
|
|
||||||
expect(result).toEqual({ success: false, message: '无法获取当前标签页' });
|
expect(result).toEqual({ success: false, message: '无法获取当前标签页' });
|
||||||
expect(consoleSpy).toHaveBeenCalledWith(
|
expect(consoleSpy).toHaveBeenCalledWith(
|
||||||
'[Messaging] 无法获取当前标签页,无法发送动作: reloadTab',
|
'[Messaging] 无法获取当前标签页,无法发送动作: restoreRightClick',
|
||||||
);
|
);
|
||||||
consoleSpy.mockRestore();
|
consoleSpy.mockRestore();
|
||||||
});
|
});
|
||||||
@@ -73,7 +77,7 @@ describe('messages', () => {
|
|||||||
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||||
(chrome.tabs.query as any).mockResolvedValue([{ url: 'https://example.com' }]);
|
(chrome.tabs.query as any).mockResolvedValue([{ url: 'https://example.com' }]);
|
||||||
|
|
||||||
const result = await sendMessageToContent(MessageAction.RELOAD_TAB, { tabId: 123 });
|
const result = await sendMessageToContent(MessageAction.RESTORE_RIGHT_CLICK);
|
||||||
|
|
||||||
expect(result).toEqual({ success: false, message: '无法获取当前标签页' });
|
expect(result).toEqual({ success: false, message: '无法获取当前标签页' });
|
||||||
consoleSpy.mockRestore();
|
consoleSpy.mockRestore();
|
||||||
@@ -87,7 +91,7 @@ describe('messages', () => {
|
|||||||
const mockTab = { id: 123, url: 'https://example.com' };
|
const mockTab = { id: 123, url: 'https://example.com' };
|
||||||
(chrome.tabs.query as any).mockResolvedValue([mockTab]);
|
(chrome.tabs.query as any).mockResolvedValue([mockTab]);
|
||||||
|
|
||||||
const result = await sendMessageToContent(MessageAction.RELOAD_TAB, { tabId: 123 });
|
const result = await sendMessageToContent(MessageAction.RESTORE_RIGHT_CLICK);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
success: false,
|
success: false,
|
||||||
@@ -104,7 +108,7 @@ describe('messages', () => {
|
|||||||
const mockTab = { id: 123, url: 'https://example.com' };
|
const mockTab = { id: 123, url: 'https://example.com' };
|
||||||
(chrome.tabs.query as any).mockResolvedValue([mockTab]);
|
(chrome.tabs.query as any).mockResolvedValue([mockTab]);
|
||||||
|
|
||||||
const result = await sendMessageToContent(MessageAction.RELOAD_TAB, { tabId: 123 });
|
const result = await sendMessageToContent(MessageAction.RESTORE_RIGHT_CLICK);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
success: false,
|
success: false,
|
||||||
@@ -121,7 +125,7 @@ describe('messages', () => {
|
|||||||
const mockTab = { id: 123, url: 'https://example.com' };
|
const mockTab = { id: 123, url: 'https://example.com' };
|
||||||
(chrome.tabs.query as any).mockResolvedValue([mockTab]);
|
(chrome.tabs.query as any).mockResolvedValue([mockTab]);
|
||||||
|
|
||||||
const result = await sendMessageToContent(MessageAction.RELOAD_TAB, { tabId: 123 });
|
const result = await sendMessageToContent(MessageAction.RESTORE_RIGHT_CLICK);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
success: false,
|
success: false,
|
||||||
@@ -138,7 +142,7 @@ describe('messages', () => {
|
|||||||
const mockTab = { id: 123, url: 'https://example.com' };
|
const mockTab = { id: 123, url: 'https://example.com' };
|
||||||
(chrome.tabs.query as any).mockResolvedValue([mockTab]);
|
(chrome.tabs.query as any).mockResolvedValue([mockTab]);
|
||||||
|
|
||||||
const result = await sendMessageToContent(MessageAction.RELOAD_TAB, { tabId: 123 });
|
const result = await sendMessageToContent(MessageAction.RESTORE_RIGHT_CLICK);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
success: false,
|
success: false,
|
||||||
@@ -151,7 +155,7 @@ describe('messages', () => {
|
|||||||
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
(chrome.tabs.query as any).mockRejectedValue(new Error('Query failed'));
|
(chrome.tabs.query as any).mockRejectedValue(new Error('Query failed'));
|
||||||
|
|
||||||
const result = await sendMessageToContent(MessageAction.RELOAD_TAB, { tabId: 123 });
|
const result = await sendMessageToContent(MessageAction.RESTORE_RIGHT_CLICK);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
success: false,
|
success: false,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { defineExtensionMessaging } from '@webext-core/messaging';
|
import { defineExtensionMessaging } from '@webext-core/messaging';
|
||||||
|
|
||||||
export enum MessageAction {
|
export enum MessageAction {
|
||||||
RELOAD_TAB = 'reloadTab',
|
|
||||||
SIDE_PANEL_STATE_CHANGED = 'sidePanelStateChanged',
|
SIDE_PANEL_STATE_CHANGED = 'sidePanelStateChanged',
|
||||||
CONTEXT_MENU_CLICKED = 'contextMenuClicked',
|
CONTEXT_MENU_CLICKED = 'contextMenuClicked',
|
||||||
RESTORE_RIGHT_CLICK = 'restoreRightClick',
|
RESTORE_RIGHT_CLICK = 'restoreRightClick',
|
||||||
@@ -21,7 +20,6 @@ export interface ContextMenuClickedPayload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ProtocolMap {
|
export interface ProtocolMap {
|
||||||
[MessageAction.RELOAD_TAB](data: { tabId: number; delay?: number }): MessageResponse;
|
|
||||||
[MessageAction.SIDE_PANEL_STATE_CHANGED](data: { isOpen: boolean }): void;
|
[MessageAction.SIDE_PANEL_STATE_CHANGED](data: { isOpen: boolean }): void;
|
||||||
[MessageAction.CONTEXT_MENU_CLICKED](data: ContextMenuClickedPayload): void;
|
[MessageAction.CONTEXT_MENU_CLICKED](data: ContextMenuClickedPayload): void;
|
||||||
[MessageAction.RESTORE_RIGHT_CLICK](data: undefined): MessageResponse & { restored: boolean };
|
[MessageAction.RESTORE_RIGHT_CLICK](data: undefined): MessageResponse & { restored: boolean };
|
||||||
|
|||||||
+36
-19
@@ -215,36 +215,43 @@ export async function clearCookies(url: string): Promise<StorageCleanResult> {
|
|||||||
async function runCleanScript(
|
async function runCleanScript(
|
||||||
tabId: number,
|
tabId: number,
|
||||||
func: () => { count: number } | Promise<{ count: number }>,
|
func: () => { count: number } | Promise<{ count: number }>,
|
||||||
|
errorLabel: string,
|
||||||
): Promise<StorageCleanResult> {
|
): Promise<StorageCleanResult> {
|
||||||
try {
|
const raw = await runScript(tabId, func, errorLabel, { count: 0 });
|
||||||
const [result] = await chrome.scripting.executeScript({ target: { tabId }, func });
|
if (raw && typeof raw === 'object' && 'count' in raw) {
|
||||||
if (result?.result && typeof result.result === 'object' && 'count' in result.result) {
|
return { success: true, count: raw.count };
|
||||||
return { success: true, count: result.result.count };
|
|
||||||
}
|
}
|
||||||
return { success: false, error: 'No result returned' };
|
return { success: false, error: 'No result returned' };
|
||||||
} catch (error) {
|
|
||||||
return { success: false, error: String(error) };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function injectClearLocalStorage(tabId: number): Promise<StorageCleanResult> {
|
export async function injectClearLocalStorage(tabId: number): Promise<StorageCleanResult> {
|
||||||
return runCleanScript(tabId, () => {
|
return runCleanScript(
|
||||||
|
tabId,
|
||||||
|
() => {
|
||||||
const count = localStorage.length;
|
const count = localStorage.length;
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
return { count };
|
return { count };
|
||||||
});
|
},
|
||||||
|
'clear LocalStorage',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function injectClearSessionStorage(tabId: number): Promise<StorageCleanResult> {
|
export async function injectClearSessionStorage(tabId: number): Promise<StorageCleanResult> {
|
||||||
return runCleanScript(tabId, () => {
|
return runCleanScript(
|
||||||
|
tabId,
|
||||||
|
() => {
|
||||||
const count = sessionStorage.length;
|
const count = sessionStorage.length;
|
||||||
sessionStorage.clear();
|
sessionStorage.clear();
|
||||||
return { count };
|
return { count };
|
||||||
});
|
},
|
||||||
|
'clear SessionStorage',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function injectClearIndexedDB(tabId: number): Promise<StorageCleanResult> {
|
export async function injectClearIndexedDB(tabId: number): Promise<StorageCleanResult> {
|
||||||
return runCleanScript(tabId, async () => {
|
return runCleanScript(
|
||||||
|
tabId,
|
||||||
|
async () => {
|
||||||
if (typeof indexedDB.databases !== 'function') {
|
if (typeof indexedDB.databases !== 'function') {
|
||||||
return { count: 0 };
|
return { count: 0 };
|
||||||
}
|
}
|
||||||
@@ -280,11 +287,15 @@ export async function injectClearIndexedDB(tabId: number): Promise<StorageCleanR
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { count };
|
return { count };
|
||||||
});
|
},
|
||||||
|
'clear IndexedDB',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function injectClearCacheStorage(tabId: number): Promise<StorageCleanResult> {
|
export async function injectClearCacheStorage(tabId: number): Promise<StorageCleanResult> {
|
||||||
return runCleanScript(tabId, async () => {
|
return runCleanScript(
|
||||||
|
tabId,
|
||||||
|
async () => {
|
||||||
if ('caches' in window) {
|
if ('caches' in window) {
|
||||||
const cacheNames = await caches.keys();
|
const cacheNames = await caches.keys();
|
||||||
for (const name of cacheNames) {
|
for (const name of cacheNames) {
|
||||||
@@ -293,11 +304,15 @@ export async function injectClearCacheStorage(tabId: number): Promise<StorageCle
|
|||||||
return { count: cacheNames.length };
|
return { count: cacheNames.length };
|
||||||
}
|
}
|
||||||
return { count: 0 };
|
return { count: 0 };
|
||||||
});
|
},
|
||||||
|
'clear CacheStorage',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function injectUnregisterServiceWorkers(tabId: number): Promise<StorageCleanResult> {
|
export async function injectUnregisterServiceWorkers(tabId: number): Promise<StorageCleanResult> {
|
||||||
return runCleanScript(tabId, async () => {
|
return runCleanScript(
|
||||||
|
tabId,
|
||||||
|
async () => {
|
||||||
if ('serviceWorker' in navigator) {
|
if ('serviceWorker' in navigator) {
|
||||||
const registrations = await navigator.serviceWorker.getRegistrations();
|
const registrations = await navigator.serviceWorker.getRegistrations();
|
||||||
for (const registration of registrations) {
|
for (const registration of registrations) {
|
||||||
@@ -306,7 +321,9 @@ export async function injectUnregisterServiceWorkers(tabId: number): Promise<Sto
|
|||||||
return { count: registrations.length };
|
return { count: registrations.length };
|
||||||
}
|
}
|
||||||
return { count: 0 };
|
return { count: 0 };
|
||||||
});
|
},
|
||||||
|
'unregister ServiceWorkers',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function clearStorage(
|
export async function clearStorage(
|
||||||
@@ -314,7 +331,7 @@ export async function clearStorage(
|
|||||||
url: string,
|
url: string,
|
||||||
options: StorageCleanerOptions,
|
options: StorageCleanerOptions,
|
||||||
): Promise<CleaningResult> {
|
): Promise<CleaningResult> {
|
||||||
const result: CleaningResult = { success: true };
|
const result: CleaningResult = { overallSuccess: true };
|
||||||
|
|
||||||
if (options.localStorage) {
|
if (options.localStorage) {
|
||||||
result.localStorage = await injectClearLocalStorage(tabId);
|
result.localStorage = await injectClearLocalStorage(tabId);
|
||||||
@@ -339,7 +356,7 @@ export async function clearStorage(
|
|||||||
(r): r is StorageCleanResult => r?.success === false,
|
(r): r is StorageCleanResult => r?.success === false,
|
||||||
);
|
);
|
||||||
if (failures.length > 0) {
|
if (failures.length > 0) {
|
||||||
result.success = false;
|
result.overallSuccess = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
+1
-2
@@ -125,7 +125,7 @@ const webExtensionMock = {
|
|||||||
get: vi.fn().mockResolvedValue({}),
|
get: vi.fn().mockResolvedValue({}),
|
||||||
sendMessage: vi.fn().mockResolvedValue(undefined),
|
sendMessage: vi.fn().mockResolvedValue(undefined),
|
||||||
create: vi.fn().mockResolvedValue({}),
|
create: vi.fn().mockResolvedValue({}),
|
||||||
reload: vi.fn().mockResolvedValue(undefined), // ✅ 承接 MessageAction.RELOAD_TAB 刷新单元测试
|
reload: vi.fn().mockResolvedValue(undefined),
|
||||||
},
|
},
|
||||||
runtime: {
|
runtime: {
|
||||||
id: 'test-extension-id',
|
id: 'test-extension-id',
|
||||||
@@ -202,7 +202,6 @@ afterEach(() => {
|
|||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 全局 matchMedia 极客级环境模拟(ThemeModeProvider 依赖)
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
Object.defineProperty(window, 'matchMedia', {
|
Object.defineProperty(window, 'matchMedia', {
|
||||||
writable: true,
|
writable: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user