refactor: 首次运行为空列表并简化存储初始化

- 移除 seed-data 模块,首次运行改为持久化空任务列表
- 简化 initializeStore(),将 tasksInitialized 处理集中到 store 层
- 移除 loadTasksData() 写入 tasksInitialized 的副作用
- 已归一化的任务数据跳过多余的 localStorage 回写
- 为旧版安装补写 tasksInitialized;首次写入失败时下次访问重试
- 补充存储兼容性测试并更新 README
This commit is contained in:
eddy
2026-08-01 14:53:32 +08:00
parent ebc245da50
commit f1c6c052bc
6 changed files with 78 additions and 132 deletions
+58 -21
View File
@@ -32,7 +32,6 @@ import {
setSortOrder,
updateTask,
} from '../src/js/store.js';
import { createSeedTasks } from '../src/js/seed-data.js';
const base = (id, title, extra = {}) => ({
id,
@@ -46,7 +45,10 @@ const base = (id, title, extra = {}) => ({
...extra,
});
beforeEach(() => localStorage.clear());
beforeEach(() => {
localStorage.clear();
consumeStorageWriteFailure();
});
afterEach(() => vi.restoreAllMocks());
describe('task model', () => {
@@ -153,28 +155,62 @@ describe('due dates', () => {
});
});
describe('seed data', () => {
it('creates five unique tasks at the initialization time', () => {
const now = new Date('2026-07-19T02:54:00.000Z');
const tasks = createSeedTasks(now);
expect(tasks).toHaveLength(5);
expect(new Set(tasks.map((task) => task.id)).size).toBe(5);
expect(tasks.every((task) => task.createdDate === now.toISOString())).toBe(true);
});
});
describe('storage and store compatibility', () => {
it('distinguishes a missing task key from an intentionally empty task list', () => {
expect(loadTasksData()).toMatchObject({ tasks: [], corrupted: false, exists: false });
localStorage.setItem('tasks', '[]');
expect(loadTasksData()).toMatchObject({ tasks: [], corrupted: false, exists: true });
});
it('preserves corrupted raw data without replacing it with seed tasks', () => {
it('backfills tasksInitialized for legacy installs that already have tasks', () => {
localStorage.setItem('tasks', JSON.stringify([base(1, 'Legacy')]));
expect(isInitialized()).toBe(false);
initializeStore();
expect(isInitialized()).toBe(true);
expect(getTasks()).toMatchObject([{ id: 1, title: 'Legacy' }]);
});
it('starts with an empty task list on first use', () => {
initializeStore();
expect(getTasks()).toEqual([]);
expect(localStorage.getItem('tasks')).toBe('[]');
expect(isInitialized()).toBe(true);
expect(consumeStorageWriteFailure()).toBe(false);
});
it('skips the redundant rewrite when stored tasks are already normalized', () => {
initializeStore();
addTask(base(1, 'Kept'));
const setItem = vi.spyOn(Storage.prototype, 'setItem');
initializeStore();
const taskWrites = setItem.mock.calls.filter(([key]) => key === 'tasks');
expect(taskWrites).toHaveLength(0);
expect(getTasks()).toMatchObject([{ id: 1, title: 'Kept' }]);
});
it('does not mark initialized when first persist fails', () => {
vi.spyOn(Storage.prototype, 'setItem').mockImplementation(() => {
throw new DOMException('full', 'QuotaExceededError');
});
initializeStore();
expect(getTasks()).toEqual([]);
expect(localStorage.getItem('tasks')).toBeNull();
expect(isInitialized()).toBe(false);
expect(consumeStorageWriteFailure()).toBe(true);
});
it('attempts no write at all when storage is unavailable', () => {
vi.spyOn(Storage.prototype, 'getItem').mockImplementation(() => {
throw new DOMException('blocked', 'SecurityError');
});
const setItem = vi.spyOn(Storage.prototype, 'setItem');
initializeStore();
expect(getTasks()).toEqual([]);
expect(setItem).not.toHaveBeenCalled();
expect(consumeStorageWriteFailure()).toBe(false);
});
it('preserves corrupted raw data without replacing it', () => {
localStorage.setItem('tasks', '{broken');
initializeStore([base(1, 'Seed')], false);
initializeStore();
expect(getTasks()).toEqual([]);
expect(localStorage.getItem('tasks')).toBe('{broken');
expect(localStorage.getItem('tasksCorruptedBackup')).toBe('{broken');
expect(isInitialized()).toBe(false);
});
it('restores all legacy preferences and task fields', () => {
localStorage.setItem('tasks', JSON.stringify(legacyBackup.data.tasks));
@@ -183,8 +219,9 @@ describe('storage and store compatibility', () => {
localStorage.setItem('showHiddenTodoTasks', 'false');
localStorage.setItem('tasksInitialized', 'true');
localStorage.setItem('tasksCorruptedBackup', '{historical backup');
initializeStore([], isInitialized());
initializeStore();
expect(getTasks()).toMatchObject(legacyBackup.data.tasks);
expect(JSON.parse(localStorage.getItem('tasks'))).toEqual(getTasks());
expect(getSortOrders()).toEqual(legacyBackup.data.sortOrders);
expect(getShowHidden('completed')).toBe(true);
expect(getShowHidden('todo')).toBe(false);
@@ -193,7 +230,7 @@ describe('storage and store compatibility', () => {
expect(localStorage.getItem('tasksCorruptedBackup')).toBe('{historical backup');
});
it('maintains store ids and rejects invalid preference mutations', () => {
initializeStore([], true);
initializeStore();
const first = addTask(base(1, 'First'));
const duplicate = addTask(base(1, 'Second'));
const [appended] = appendTasks([base(first.id, 'Third')]);
@@ -213,7 +250,7 @@ describe('storage and store compatibility', () => {
vi.spyOn(Storage.prototype, 'setItem').mockImplementation(() => {
throw new DOMException('full', 'QuotaExceededError');
});
expect(() => initializeStore([base(1, 'Seed')], false)).not.toThrow();
expect(() => initializeStore()).not.toThrow();
expect(consumeStorageWriteFailure()).toBe(true);
expect(consumeStorageWriteFailure()).toBe(false);
});
@@ -237,7 +274,7 @@ describe('imports', () => {
Modal: { getOrCreateInstance: () => ({ hide: vi.fn(), show: vi.fn() }) },
};
document.body.innerHTML = '<div id="importModal"></div>';
initializeStore([], true);
initializeStore();
});
it('validates the legacy export fixture', () => {
expect(validateImportData(legacyBackup)).toBe(true);
@@ -296,7 +333,7 @@ describe('imports', () => {
});
describe('timeline entries', () => {
beforeEach(() => initializeStore([], true));
beforeEach(() => initializeStore());
it('ignores fields that the change set omits', () => {
const task = addTask(base(1, 'Untouched', { assignee: 'Ada', dueDate: '2025-01-01' }));
@@ -367,7 +404,7 @@ describe('board rendering', () => {
beforeEach(() => {
document.body.innerHTML = fixture();
initializeStore([], true);
initializeStore();
});
it('lists timeline entries newest first within a single day', () => {
@@ -474,7 +511,7 @@ describe('export', () => {
original.createObjectURL = URL.createObjectURL;
original.revokeObjectURL = URL.revokeObjectURL;
document.body.innerHTML = '';
initializeStore([], true);
initializeStore();
});
afterEach(() => {
URL.createObjectURL = original.createObjectURL;