refactor(代码重复): 抽取共享工具函数与 UI 组件,消除多处重复实现
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -77,15 +77,15 @@ describe('chromeStorage', () => {
|
||||
|
||||
describe('get 类型签名', () => {
|
||||
it('无默认值时应推断为可选返回类型', () => {
|
||||
const getWithoutDefault = () => storageUtil.get('app/theme');
|
||||
expectTypeOf<ReturnType<typeof getWithoutDefault>>().toEqualTypeOf<
|
||||
const _getWithoutDefault = () => storageUtil.get('app/theme');
|
||||
expectTypeOf<ReturnType<typeof _getWithoutDefault>>().toEqualTypeOf<
|
||||
Promise<string | undefined>
|
||||
>();
|
||||
});
|
||||
|
||||
it('有默认值时应推断为确定返回类型', () => {
|
||||
const getWithDefault = () => storageUtil.get('app/theme', 'light');
|
||||
expectTypeOf<ReturnType<typeof getWithDefault>>().toEqualTypeOf<Promise<string>>();
|
||||
const _getWithDefault = () => storageUtil.get('app/theme', 'light');
|
||||
expectTypeOf<ReturnType<typeof _getWithDefault>>().toEqualTypeOf<Promise<string>>();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import {
|
||||
isRestrictedUrl,
|
||||
isUnsupportedPageUrl,
|
||||
RESTRICTED_PROTOCOLS,
|
||||
} from '@/utils/restrictedUrls';
|
||||
|
||||
describe('restrictedUrls', () => {
|
||||
describe('isRestrictedUrl', () => {
|
||||
it('应识别受限协议页面', () => {
|
||||
expect(isRestrictedUrl('chrome://settings')).toBe(true);
|
||||
expect(isRestrictedUrl('chrome-extension://abc123/background.html')).toBe(true);
|
||||
expect(isRestrictedUrl('about:blank')).toBe(true);
|
||||
expect(isRestrictedUrl('edge://settings')).toBe(true);
|
||||
expect(isRestrictedUrl('brave://settings')).toBe(true);
|
||||
expect(isRestrictedUrl('view-source:https://example.com')).toBe(true);
|
||||
expect(isRestrictedUrl('file:///path/to/file')).toBe(true);
|
||||
expect(isRestrictedUrl('data:text/html,<h1>Hello</h1>')).toBe(true);
|
||||
});
|
||||
|
||||
it('应允许普通 http/https 页面', () => {
|
||||
expect(isRestrictedUrl('http://example.com')).toBe(false);
|
||||
expect(isRestrictedUrl('https://example.com')).toBe(false);
|
||||
});
|
||||
|
||||
it('空 URL 应视为受限', () => {
|
||||
expect(isRestrictedUrl(undefined)).toBe(true);
|
||||
expect(isRestrictedUrl('')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isUnsupportedPageUrl', () => {
|
||||
it('应通过 protocol 精确匹配识别受限页面', () => {
|
||||
expect(isUnsupportedPageUrl('chrome://newtab/')).toBe(true);
|
||||
expect(isUnsupportedPageUrl('brave://settings/')).toBe(true);
|
||||
expect(isUnsupportedPageUrl('https://example.com')).toBe(false);
|
||||
});
|
||||
|
||||
it('无效 URL 应视为不支持', () => {
|
||||
expect(isUnsupportedPageUrl(undefined)).toBe(true);
|
||||
expect(isUnsupportedPageUrl('not-a-url')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('RESTRICTED_PROTOCOLS 应包含 storage cleaner 与右键恢复所需协议', () => {
|
||||
expect(RESTRICTED_PROTOCOLS).toEqual(
|
||||
expect.arrayContaining(['brave:', 'view-source:', 'file:', 'data:']),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { getSyncSnapshot } from '@/utils/syncSnapshot';
|
||||
|
||||
describe('getSyncSnapshot', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('无快照时应返回默认值', () => {
|
||||
expect(getSyncSnapshot('app/test-key', 'default')).toBe('default');
|
||||
});
|
||||
|
||||
it('应读取并解析合法 JSON 快照', () => {
|
||||
localStorage.setItem('snapshot/app/test-key', JSON.stringify('saved'));
|
||||
expect(getSyncSnapshot('app/test-key', 'default')).toBe('saved');
|
||||
});
|
||||
|
||||
it('validator 失败时应回退到默认值', () => {
|
||||
localStorage.setItem('snapshot/app/test-key', JSON.stringify('invalid'));
|
||||
const isNumber = (val: unknown): val is number => typeof val === 'number';
|
||||
expect(getSyncSnapshot('app/test-key', 0, isNumber)).toBe(0);
|
||||
});
|
||||
|
||||
it('非法 JSON 时应回退到默认值并记录错误', () => {
|
||||
localStorage.setItem('snapshot/app/test-key', '{invalid');
|
||||
expect(getSyncSnapshot('app/test-key', 'fallback')).toBe('fallback');
|
||||
expect(console.error).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
/** 浏览器内部/受限协议(含尾部冒号,用于 protocol 匹配) */
|
||||
export const RESTRICTED_PROTOCOLS = [
|
||||
'chrome:',
|
||||
'chrome-extension:',
|
||||
'about:',
|
||||
'edge:',
|
||||
'brave:',
|
||||
'view-source:',
|
||||
'file:',
|
||||
'data:',
|
||||
] as const;
|
||||
|
||||
export function getUrlProtocol(url: string): string | null {
|
||||
try {
|
||||
return new URL(url).protocol;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** 用于 content script / 右键恢复等(protocol 精确匹配) */
|
||||
export function isUnsupportedPageUrl(url: string | undefined): boolean {
|
||||
if (!url) return true;
|
||||
const protocol = getUrlProtocol(url);
|
||||
if (!protocol) return true;
|
||||
return (RESTRICTED_PROTOCOLS as readonly string[]).includes(protocol);
|
||||
}
|
||||
|
||||
/** 用于 storage cleaner tab 检测(前缀匹配,兼容无 protocol 的场景) */
|
||||
export function isRestrictedUrl(url?: string): boolean {
|
||||
if (!url) return true;
|
||||
return RESTRICTED_PROTOCOLS.some((p) => url.startsWith(p));
|
||||
}
|
||||
@@ -1,33 +1,6 @@
|
||||
import type { CleaningResult, StorageCleanerOptions, StorageCleanResult } from '@/types/storage';
|
||||
|
||||
const RESTRICTED_PROTOCOLS = [
|
||||
'chrome:',
|
||||
'chrome-extension:',
|
||||
'about:',
|
||||
'edge:',
|
||||
'view-source:',
|
||||
'file:',
|
||||
'data:',
|
||||
] as const;
|
||||
|
||||
/** 清理选项的 key 列表(用于遍历结果) */
|
||||
const CLEAN_OPTION_KEYS: (keyof StorageCleanerOptions)[] = [
|
||||
'localStorage',
|
||||
'sessionStorage',
|
||||
'indexedDB',
|
||||
'cookies',
|
||||
'cacheStorage',
|
||||
'serviceWorkers',
|
||||
];
|
||||
|
||||
const OPTION_LABELS: Record<string, string> = {
|
||||
localStorage: 'Local Storage',
|
||||
sessionStorage: 'Session Storage',
|
||||
indexedDB: '站点存储',
|
||||
cookies: 'Cookies',
|
||||
cacheStorage: 'Cache Storage',
|
||||
serviceWorkers: 'Service Workers',
|
||||
};
|
||||
import { CLEAN_OPTION_KEYS, OPTION_LABELS } from '@/pages/StorageCleaner/constants';
|
||||
export { isRestrictedUrl } from '@/utils/restrictedUrls';
|
||||
|
||||
export async function getCurrentTab() {
|
||||
// For popup pages, we need to get the active tab from the browser window that triggered the popup.
|
||||
@@ -52,11 +25,6 @@ export async function getCurrentTab() {
|
||||
return fallbackTab;
|
||||
}
|
||||
|
||||
export function isRestrictedUrl(url?: string): boolean {
|
||||
if (!url) return true;
|
||||
return RESTRICTED_PROTOCOLS.some((p) => url.startsWith(p));
|
||||
}
|
||||
|
||||
export async function getCookieSize(url: string): Promise<number> {
|
||||
try {
|
||||
const cookies = await chrome.cookies.getAll({ url });
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* 从 localStorage 获取同步快照(用于消除异步加载产生的首屏闪烁)
|
||||
*/
|
||||
export function getSyncSnapshot<T>(
|
||||
key: string,
|
||||
defaultValue: T,
|
||||
validator?: (val: unknown) => val is T,
|
||||
): T {
|
||||
try {
|
||||
const val = localStorage.getItem(`snapshot/${key}`);
|
||||
if (!val) return defaultValue;
|
||||
const parsed = JSON.parse(val) as unknown;
|
||||
if (validator) {
|
||||
return validator(parsed) ? parsed : defaultValue;
|
||||
}
|
||||
return (parsed as T) ?? defaultValue;
|
||||
} catch (error) {
|
||||
console.error(`[SyncSnapshot] Failed to read snapshot/${key}:`, error);
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,8 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { storageUtil } from '@/utils/chromeStorage';
|
||||
import { getSyncSnapshot } from '@/utils/syncSnapshot';
|
||||
import type { StorageSchema } from '@/types/storage';
|
||||
|
||||
/**
|
||||
* 从 localStorage 获取同步快照(用于消除异步加载产生的首屏闪烁)
|
||||
*/
|
||||
const getSyncSnapshot = <T>(
|
||||
key: string,
|
||||
defaultValue: T,
|
||||
validator?: (val: unknown) => val is T,
|
||||
): T => {
|
||||
try {
|
||||
const val = localStorage.getItem(`snapshot/${key}`);
|
||||
if (!val) return defaultValue;
|
||||
const parsed = JSON.parse(val) as unknown;
|
||||
if (validator) {
|
||||
return validator(parsed) ? parsed : defaultValue;
|
||||
}
|
||||
return (parsed as T) ?? defaultValue;
|
||||
} catch {
|
||||
return defaultValue;
|
||||
}
|
||||
};
|
||||
|
||||
export const useStorageState = <K extends keyof StorageSchema>(
|
||||
key: K,
|
||||
defaultValue: StorageSchema[K],
|
||||
|
||||
Reference in New Issue
Block a user