This commit is contained in:
2026-10-21 14:42:59 +08:00
commit 954236d3f9
399 changed files with 85885 additions and 0 deletions

View File

@@ -0,0 +1,108 @@
// tests/unit/renderer/lib/order-persistence-localstorage.test.ts
//
// 守 order-persistence.ts 的「永不抛」契约(2026-10-20,r197)。
//
// store/notes.ts + store/literature.ts 的初始 state 里有
// `displayOrderById: loadOrder('note')` —— **模块 init 时**就调 loadOrder。
// 旧实现的 `typeof localStorage === 'undefined'` 探测在 try/catch **外面**:
//
// - Chromium 对不透明 origin(file:// / 隐私模式 / 站点数据被禁)访问
// localStorage 会抛 SecurityError,而 `typeof` 只挡 ReferenceError,
// **不挡 getter 抛异常** → 探测本身就炸。
// - localStorage.getItem 也在 try/catch 外面 → quota / 禁用 → 同样炸。
//
// 后果不是「排序没保存」这种小痛:store 模块 init 抛 ⇒ 整个 store 模块
// 加载失败 ⇒ 整个侧栏/列表白屏,且用户完全不知道原因。
//
// store 里的注释已经写着「任何 throw 都被 loadOrder 内部防御吞掉,返回 {}
// 兜底」—— 那条不变式此前**代码并没有兑现**。本测把注释变成可执行契约。
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { loadOrder, saveOrder } from '../../../../src/renderer/src/lib/order-persistence';
type G = { localStorage?: Storage };
function setLocalStorage(value: unknown): void {
Object.defineProperty(globalThis, 'localStorage', {
configurable: true,
get() {
return value;
},
});
}
describe('order-persistence localStorage 不可用(r197)', () => {
let original: PropertyDescriptor | undefined;
beforeEach(() => {
original = Object.getOwnPropertyDescriptor(globalThis, 'localStorage');
});
afterEach(() => {
if (original) {
Object.defineProperty(globalThis, 'localStorage', original);
} else {
Reflect.deleteProperty(globalThis as G, 'localStorage');
}
});
it('访问 localStorage 本身抛异常 → loadOrder 返 {} 不抛', () => {
Object.defineProperty(globalThis, 'localStorage', {
configurable: true,
get() {
throw new Error('SecurityError: localStorage is disabled');
},
});
expect(() => loadOrder('note')).not.toThrow();
expect(loadOrder('note')).toEqual({});
});
it('访问 localStorage 本身抛异常 → saveOrder 不抛', () => {
Object.defineProperty(globalThis, 'localStorage', {
configurable: true,
get() {
throw new Error('SecurityError: localStorage is disabled');
},
});
expect(() => saveOrder('note', { a: 1 })).not.toThrow();
});
it('getItem 抛异常(配额/禁用)→ loadOrder 返 {} 不抛', () => {
setLocalStorage({
getItem: () => {
throw new Error('quota exceeded');
},
setItem: () => {
throw new Error('quota exceeded');
},
} as unknown as Storage);
expect(() => loadOrder('literature')).not.toThrow();
expect(loadOrder('literature')).toEqual({});
});
it('setItem 抛异常 → saveOrder 静默吞掉', () => {
setLocalStorage({
getItem: () => null,
setItem: () => {
throw new Error('quota exceeded');
},
} as unknown as Storage);
expect(() => saveOrder('note', { a: 1 })).not.toThrow();
});
it('localStorage 完全不存在 → 仍返 {} / 不抛', () => {
Reflect.deleteProperty(globalThis as G, 'localStorage');
expect(loadOrder('note')).toEqual({});
expect(() => saveOrder('note', { a: 1 })).not.toThrow();
});
it('正常路径不变:读回写进去的排序', () => {
const store = new Map<string, string>();
setLocalStorage({
getItem: (k: string) => store.get(k) ?? null,
setItem: (k: string, v: string) => void store.set(k, v),
} as unknown as Storage);
saveOrder('note', { 'n1': 1, 'n2': 2 });
expect(loadOrder('note')).toEqual({ n1: 1, n2: 2 });
});
});