refactor: 首次运行为空列表并简化存储初始化
- 移除 seed-data 模块,首次运行改为持久化空任务列表 - 简化 initializeStore(),将 tasksInitialized 处理集中到 store 层 - 移除 loadTasksData() 写入 tasksInitialized 的副作用 - 已归一化的任务数据跳过多余的 localStorage 回写 - 为旧版安装补写 tasksInitialized;首次写入失败时下次访问重试 - 补充存储兼容性测试并更新 README
This commit is contained in:
+58
-21
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user