feat: proper tab handling
This commit is contained in:
342
tests/renderer/store/tabStore.test.ts
Normal file
342
tests/renderer/store/tabStore.test.ts
Normal file
@@ -0,0 +1,342 @@
|
||||
/**
|
||||
* Tests for tab management in the app store
|
||||
* Validates tabbed interface behavior: transient tabs, pinned tabs, persistence
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { useAppStore, PostData, MediaData, Tab } from '../../../src/renderer/store/appStore';
|
||||
|
||||
// Helper to create a mock post
|
||||
const createMockPost = (overrides: Partial<PostData> = {}): PostData => ({
|
||||
id: `post-${Date.now()}-${Math.random().toString(36).substring(7)}`,
|
||||
title: 'Test Post',
|
||||
slug: 'test-post',
|
||||
content: '# Test Content',
|
||||
status: 'draft',
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
tags: [],
|
||||
categories: [],
|
||||
...overrides,
|
||||
});
|
||||
|
||||
// Helper to create a mock media
|
||||
const createMockMedia = (overrides: Partial<MediaData> = {}): MediaData => ({
|
||||
id: `media-${Date.now()}-${Math.random().toString(36).substring(7)}`,
|
||||
filename: 'test.jpg',
|
||||
originalName: 'test.jpg',
|
||||
mimeType: 'image/jpeg',
|
||||
size: 1024,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
tags: [],
|
||||
...overrides,
|
||||
});
|
||||
|
||||
// Direct store access without React rendering
|
||||
const getStore = () => useAppStore.getState();
|
||||
const setState = useAppStore.setState;
|
||||
|
||||
describe('Tab Management', () => {
|
||||
beforeEach(() => {
|
||||
// Reset store state before each test
|
||||
setState({
|
||||
tabs: [],
|
||||
activeTabId: null,
|
||||
posts: [],
|
||||
media: [],
|
||||
dirtyPosts: new Set(),
|
||||
});
|
||||
});
|
||||
|
||||
describe('Opening Tabs', () => {
|
||||
it('should open a post in a transient tab on single click', () => {
|
||||
const post = createMockPost({ id: 'post-1', title: 'Test Post' });
|
||||
getStore().addPost(post);
|
||||
|
||||
getStore().openTab({ type: 'post', id: 'post-1', isTransient: true });
|
||||
|
||||
expect(getStore().tabs).toHaveLength(1);
|
||||
expect(getStore().tabs[0].type).toBe('post');
|
||||
expect(getStore().tabs[0].id).toBe('post-1');
|
||||
expect(getStore().tabs[0].isTransient).toBe(true);
|
||||
expect(getStore().activeTabId).toBe('post-1');
|
||||
});
|
||||
|
||||
it('should open a media file in a transient tab on single click', () => {
|
||||
const media = createMockMedia({ id: 'media-1' });
|
||||
getStore().addMedia(media);
|
||||
|
||||
getStore().openTab({ type: 'media', id: 'media-1', isTransient: true });
|
||||
|
||||
expect(getStore().tabs).toHaveLength(1);
|
||||
expect(getStore().tabs[0].type).toBe('media');
|
||||
expect(getStore().tabs[0].id).toBe('media-1');
|
||||
expect(getStore().tabs[0].isTransient).toBe(true);
|
||||
expect(getStore().activeTabId).toBe('media-1');
|
||||
});
|
||||
|
||||
it('should replace transient tab when opening another item with single click', () => {
|
||||
const post1 = createMockPost({ id: 'post-1', title: 'Post 1' });
|
||||
const post2 = createMockPost({ id: 'post-2', title: 'Post 2' });
|
||||
getStore().addPost(post1);
|
||||
getStore().addPost(post2);
|
||||
|
||||
getStore().openTab({ type: 'post', id: 'post-1', isTransient: true });
|
||||
getStore().openTab({ type: 'post', id: 'post-2', isTransient: true });
|
||||
|
||||
expect(getStore().tabs).toHaveLength(1);
|
||||
expect(getStore().tabs[0].id).toBe('post-2');
|
||||
expect(getStore().activeTabId).toBe('post-2');
|
||||
});
|
||||
|
||||
it('should not replace pinned tabs when opening with single click', () => {
|
||||
const post1 = createMockPost({ id: 'post-1', title: 'Post 1' });
|
||||
const post2 = createMockPost({ id: 'post-2', title: 'Post 2' });
|
||||
getStore().addPost(post1);
|
||||
getStore().addPost(post2);
|
||||
|
||||
// Double click opens pinned tab
|
||||
getStore().openTab({ type: 'post', id: 'post-1', isTransient: false });
|
||||
// Single click opens transient tab
|
||||
getStore().openTab({ type: 'post', id: 'post-2', isTransient: true });
|
||||
|
||||
expect(getStore().tabs).toHaveLength(2);
|
||||
expect(getStore().tabs[0].id).toBe('post-1');
|
||||
expect(getStore().tabs[0].isTransient).toBe(false);
|
||||
expect(getStore().tabs[1].id).toBe('post-2');
|
||||
expect(getStore().tabs[1].isTransient).toBe(true);
|
||||
});
|
||||
|
||||
it('should open a pinned tab on double click', () => {
|
||||
const post = createMockPost({ id: 'post-1', title: 'Test Post' });
|
||||
getStore().addPost(post);
|
||||
|
||||
getStore().openTab({ type: 'post', id: 'post-1', isTransient: false });
|
||||
|
||||
expect(getStore().tabs).toHaveLength(1);
|
||||
expect(getStore().tabs[0].isTransient).toBe(false);
|
||||
});
|
||||
|
||||
it('should convert transient tab to pinned on double click', () => {
|
||||
const post = createMockPost({ id: 'post-1', title: 'Test Post' });
|
||||
getStore().addPost(post);
|
||||
|
||||
// First single click - transient
|
||||
getStore().openTab({ type: 'post', id: 'post-1', isTransient: true });
|
||||
expect(getStore().tabs[0].isTransient).toBe(true);
|
||||
|
||||
// Double click - convert to pinned
|
||||
getStore().openTab({ type: 'post', id: 'post-1', isTransient: false });
|
||||
expect(getStore().tabs).toHaveLength(1);
|
||||
expect(getStore().tabs[0].isTransient).toBe(false);
|
||||
});
|
||||
|
||||
it('should switch to existing tab if already open', () => {
|
||||
const post = createMockPost({ id: 'post-1' });
|
||||
const post2 = createMockPost({ id: 'post-2' });
|
||||
getStore().addPost(post);
|
||||
getStore().addPost(post2);
|
||||
|
||||
getStore().openTab({ type: 'post', id: 'post-1', isTransient: false });
|
||||
getStore().openTab({ type: 'post', id: 'post-2', isTransient: false });
|
||||
getStore().openTab({ type: 'post', id: 'post-1', isTransient: false });
|
||||
|
||||
expect(getStore().tabs).toHaveLength(2);
|
||||
expect(getStore().activeTabId).toBe('post-1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Closing Tabs', () => {
|
||||
it('should close a tab by id', () => {
|
||||
const post = createMockPost({ id: 'post-1' });
|
||||
getStore().addPost(post);
|
||||
|
||||
getStore().openTab({ type: 'post', id: 'post-1', isTransient: false });
|
||||
getStore().closeTab('post-1');
|
||||
|
||||
expect(getStore().tabs).toHaveLength(0);
|
||||
expect(getStore().activeTabId).toBeNull();
|
||||
});
|
||||
|
||||
it('should activate the next tab when closing the active tab', () => {
|
||||
const post1 = createMockPost({ id: 'post-1' });
|
||||
const post2 = createMockPost({ id: 'post-2' });
|
||||
getStore().addPost(post1);
|
||||
getStore().addPost(post2);
|
||||
|
||||
getStore().openTab({ type: 'post', id: 'post-1', isTransient: false });
|
||||
getStore().openTab({ type: 'post', id: 'post-2', isTransient: false });
|
||||
getStore().setActiveTab('post-1');
|
||||
getStore().closeTab('post-1');
|
||||
|
||||
expect(getStore().tabs).toHaveLength(1);
|
||||
expect(getStore().activeTabId).toBe('post-2');
|
||||
});
|
||||
|
||||
it('should activate the previous tab when closing the last tab', () => {
|
||||
const post1 = createMockPost({ id: 'post-1' });
|
||||
const post2 = createMockPost({ id: 'post-2' });
|
||||
getStore().addPost(post1);
|
||||
getStore().addPost(post2);
|
||||
|
||||
getStore().openTab({ type: 'post', id: 'post-1', isTransient: false });
|
||||
getStore().openTab({ type: 'post', id: 'post-2', isTransient: false });
|
||||
getStore().closeTab('post-2');
|
||||
|
||||
expect(getStore().tabs).toHaveLength(1);
|
||||
expect(getStore().activeTabId).toBe('post-1');
|
||||
});
|
||||
|
||||
it('should not remove dirty posts from dirtyPosts when closing tab', () => {
|
||||
const post = createMockPost({ id: 'post-1' });
|
||||
getStore().addPost(post);
|
||||
getStore().markDirty('post-1');
|
||||
|
||||
getStore().openTab({ type: 'post', id: 'post-1', isTransient: false });
|
||||
getStore().closeTab('post-1');
|
||||
|
||||
// Dirty state is preserved - the post still has unsaved changes
|
||||
expect(getStore().isDirty('post-1')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tab Switching', () => {
|
||||
it('should set active tab', () => {
|
||||
const post1 = createMockPost({ id: 'post-1' });
|
||||
const post2 = createMockPost({ id: 'post-2' });
|
||||
getStore().addPost(post1);
|
||||
getStore().addPost(post2);
|
||||
|
||||
getStore().openTab({ type: 'post', id: 'post-1', isTransient: false });
|
||||
getStore().openTab({ type: 'post', id: 'post-2', isTransient: false });
|
||||
getStore().setActiveTab('post-1');
|
||||
|
||||
expect(getStore().activeTabId).toBe('post-1');
|
||||
});
|
||||
|
||||
it('should not change active tab when switching to non-existent tab', () => {
|
||||
const post = createMockPost({ id: 'post-1' });
|
||||
getStore().addPost(post);
|
||||
|
||||
getStore().openTab({ type: 'post', id: 'post-1', isTransient: false });
|
||||
getStore().setActiveTab('non-existent');
|
||||
|
||||
expect(getStore().activeTabId).toBe('post-1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Pin Tab', () => {
|
||||
it('should pin a transient tab', () => {
|
||||
const post = createMockPost({ id: 'post-1' });
|
||||
getStore().addPost(post);
|
||||
|
||||
getStore().openTab({ type: 'post', id: 'post-1', isTransient: true });
|
||||
getStore().pinTab('post-1');
|
||||
|
||||
expect(getStore().tabs[0].isTransient).toBe(false);
|
||||
});
|
||||
|
||||
it('should convert transient tab to pinned when editing starts', () => {
|
||||
const post = createMockPost({ id: 'post-1' });
|
||||
getStore().addPost(post);
|
||||
|
||||
getStore().openTab({ type: 'post', id: 'post-1', isTransient: true });
|
||||
getStore().markDirty('post-1');
|
||||
getStore().pinTab('post-1');
|
||||
|
||||
expect(getStore().tabs[0].isTransient).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tab Persistence', () => {
|
||||
it('should return tabs state for project persistence', () => {
|
||||
const post1 = createMockPost({ id: 'post-1' });
|
||||
const post2 = createMockPost({ id: 'post-2' });
|
||||
getStore().addPost(post1);
|
||||
getStore().addPost(post2);
|
||||
|
||||
getStore().openTab({ type: 'post', id: 'post-1', isTransient: false });
|
||||
getStore().openTab({ type: 'post', id: 'post-2', isTransient: false });
|
||||
|
||||
const tabState = getStore().getTabState();
|
||||
|
||||
expect(tabState.tabs).toHaveLength(2);
|
||||
expect(tabState.activeTabId).toBe('post-2');
|
||||
});
|
||||
|
||||
it('should restore tabs from persisted state', () => {
|
||||
const tabState = {
|
||||
tabs: [
|
||||
{ type: 'post' as const, id: 'post-1', isTransient: false },
|
||||
{ type: 'media' as const, id: 'media-1', isTransient: false },
|
||||
],
|
||||
activeTabId: 'media-1',
|
||||
};
|
||||
|
||||
getStore().restoreTabState(tabState);
|
||||
|
||||
expect(getStore().tabs).toHaveLength(2);
|
||||
expect(getStore().activeTabId).toBe('media-1');
|
||||
});
|
||||
|
||||
it('should clear tabs when switching projects', () => {
|
||||
const post = createMockPost({ id: 'post-1' });
|
||||
getStore().addPost(post);
|
||||
getStore().openTab({ type: 'post', id: 'post-1', isTransient: false });
|
||||
|
||||
getStore().clearTabs();
|
||||
|
||||
expect(getStore().tabs).toHaveLength(0);
|
||||
expect(getStore().activeTabId).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Settings Tab', () => {
|
||||
it('should open settings as a special tab', () => {
|
||||
getStore().openTab({ type: 'settings', id: 'settings', isTransient: false });
|
||||
|
||||
expect(getStore().tabs).toHaveLength(1);
|
||||
expect(getStore().tabs[0].type).toBe('settings');
|
||||
expect(getStore().activeTabId).toBe('settings');
|
||||
});
|
||||
|
||||
it('should not allow multiple settings tabs', () => {
|
||||
getStore().openTab({ type: 'settings', id: 'settings', isTransient: false });
|
||||
getStore().openTab({ type: 'settings', id: 'settings', isTransient: false });
|
||||
|
||||
expect(getStore().tabs).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Multiple Tab Types', () => {
|
||||
it('should handle mixed posts and media tabs', () => {
|
||||
const post = createMockPost({ id: 'post-1' });
|
||||
const media = createMockMedia({ id: 'media-1' });
|
||||
getStore().addPost(post);
|
||||
getStore().addMedia(media);
|
||||
|
||||
getStore().openTab({ type: 'post', id: 'post-1', isTransient: false });
|
||||
getStore().openTab({ type: 'media', id: 'media-1', isTransient: false });
|
||||
|
||||
expect(getStore().tabs).toHaveLength(2);
|
||||
expect(getStore().tabs[0].type).toBe('post');
|
||||
expect(getStore().tabs[1].type).toBe('media');
|
||||
});
|
||||
|
||||
it('should keep transient tabs separate by type', () => {
|
||||
const post = createMockPost({ id: 'post-1' });
|
||||
const media = createMockMedia({ id: 'media-1' });
|
||||
getStore().addPost(post);
|
||||
getStore().addMedia(media);
|
||||
|
||||
// Open transient post tab
|
||||
getStore().openTab({ type: 'post', id: 'post-1', isTransient: true });
|
||||
// Open transient media tab - should NOT replace the post tab
|
||||
getStore().openTab({ type: 'media', id: 'media-1', isTransient: true });
|
||||
|
||||
// Both transient tabs should exist since they're different types
|
||||
expect(getStore().tabs).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
249
tests/renderer/utils/autoSave.test.ts
Normal file
249
tests/renderer/utils/autoSave.test.ts
Normal file
@@ -0,0 +1,249 @@
|
||||
/**
|
||||
* Tests for auto-save functionality with idle detection
|
||||
* Validates that drafts are automatically saved after a configurable idle period
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { AutoSaveManager } from '../../../src/renderer/utils/autoSave';
|
||||
|
||||
// Mock the electronAPI
|
||||
const mockSaveDraft = vi.fn().mockResolvedValue({ id: 'post-1', title: 'Test' });
|
||||
const mockGetPost = vi.fn().mockResolvedValue({ id: 'post-1', title: 'Test', content: 'Content' });
|
||||
|
||||
describe('AutoSaveManager', () => {
|
||||
let autoSaveManager: AutoSaveManager;
|
||||
let onSaveCallback: ReturnType<typeof vi.fn>;
|
||||
let onSaveCompleteCallback: ReturnType<typeof vi.fn>;
|
||||
let onSaveErrorCallback: ReturnType<typeof vi.fn>;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
onSaveCallback = vi.fn().mockResolvedValue(undefined);
|
||||
onSaveCompleteCallback = vi.fn();
|
||||
onSaveErrorCallback = vi.fn();
|
||||
|
||||
autoSaveManager = new AutoSaveManager({
|
||||
idleTimeMs: 3000, // 3 seconds idle before save
|
||||
onSave: onSaveCallback,
|
||||
onSaveComplete: onSaveCompleteCallback,
|
||||
onSaveError: onSaveErrorCallback,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
autoSaveManager.dispose();
|
||||
vi.useRealTimers();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('Idle Detection', () => {
|
||||
it('should not save until idle time has passed', () => {
|
||||
autoSaveManager.notifyChange('post-1', { content: 'Updated content' });
|
||||
|
||||
// Advance time by less than idle time
|
||||
vi.advanceTimersByTime(2000);
|
||||
|
||||
expect(onSaveCallback).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should save after idle time has passed', async () => {
|
||||
autoSaveManager.notifyChange('post-1', { content: 'Updated content' });
|
||||
|
||||
// Advance time past idle threshold
|
||||
vi.advanceTimersByTime(3500);
|
||||
|
||||
expect(onSaveCallback).toHaveBeenCalledTimes(1);
|
||||
expect(onSaveCallback).toHaveBeenCalledWith('post-1', { content: 'Updated content' });
|
||||
});
|
||||
|
||||
it('should reset idle timer on each change', () => {
|
||||
autoSaveManager.notifyChange('post-1', { content: 'First change' });
|
||||
|
||||
// Advance time by 2 seconds
|
||||
vi.advanceTimersByTime(2000);
|
||||
|
||||
// Make another change - should reset timer
|
||||
autoSaveManager.notifyChange('post-1', { content: 'Second change' });
|
||||
|
||||
// Advance time by 2 more seconds (4 seconds since first change)
|
||||
vi.advanceTimersByTime(2000);
|
||||
|
||||
// Should not have saved yet because timer was reset
|
||||
expect(onSaveCallback).not.toHaveBeenCalled();
|
||||
|
||||
// Advance past the new idle threshold
|
||||
vi.advanceTimersByTime(1500);
|
||||
|
||||
// Now it should save
|
||||
expect(onSaveCallback).toHaveBeenCalledTimes(1);
|
||||
expect(onSaveCallback).toHaveBeenCalledWith('post-1', { content: 'Second change' });
|
||||
});
|
||||
|
||||
it('should accumulate changes before saving', () => {
|
||||
autoSaveManager.notifyChange('post-1', { content: 'Change 1' });
|
||||
vi.advanceTimersByTime(1000);
|
||||
|
||||
autoSaveManager.notifyChange('post-1', { title: 'New Title' });
|
||||
vi.advanceTimersByTime(1000);
|
||||
|
||||
autoSaveManager.notifyChange('post-1', { content: 'Change 3' });
|
||||
vi.advanceTimersByTime(3500);
|
||||
|
||||
expect(onSaveCallback).toHaveBeenCalledTimes(1);
|
||||
// Should have merged the changes
|
||||
expect(onSaveCallback).toHaveBeenCalledWith('post-1', {
|
||||
title: 'New Title',
|
||||
content: 'Change 3'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Multiple Items', () => {
|
||||
it('should track changes for multiple items independently', () => {
|
||||
autoSaveManager.notifyChange('post-1', { content: 'Post 1 content' });
|
||||
vi.advanceTimersByTime(1000);
|
||||
|
||||
autoSaveManager.notifyChange('post-2', { content: 'Post 2 content' });
|
||||
vi.advanceTimersByTime(2500);
|
||||
|
||||
// Post 1 should save first (3.5 seconds since its last change)
|
||||
expect(onSaveCallback).toHaveBeenCalledTimes(1);
|
||||
expect(onSaveCallback).toHaveBeenCalledWith('post-1', { content: 'Post 1 content' });
|
||||
|
||||
// Advance to save post 2
|
||||
vi.advanceTimersByTime(1000);
|
||||
|
||||
expect(onSaveCallback).toHaveBeenCalledTimes(2);
|
||||
expect(onSaveCallback).toHaveBeenLastCalledWith('post-2', { content: 'Post 2 content' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Force Save', () => {
|
||||
it('should immediately save all pending changes on forceSave', async () => {
|
||||
autoSaveManager.notifyChange('post-1', { content: 'Content 1' });
|
||||
autoSaveManager.notifyChange('post-2', { content: 'Content 2' });
|
||||
|
||||
await autoSaveManager.forceSave();
|
||||
|
||||
expect(onSaveCallback).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should clear pending changes after forceSave', async () => {
|
||||
autoSaveManager.notifyChange('post-1', { content: 'Content' });
|
||||
|
||||
await autoSaveManager.forceSave();
|
||||
|
||||
// Advance time - no additional saves should occur
|
||||
vi.advanceTimersByTime(5000);
|
||||
|
||||
expect(onSaveCallback).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Cancel', () => {
|
||||
it('should cancel pending save for specific item', () => {
|
||||
autoSaveManager.notifyChange('post-1', { content: 'Content' });
|
||||
autoSaveManager.cancel('post-1');
|
||||
|
||||
vi.advanceTimersByTime(5000);
|
||||
|
||||
expect(onSaveCallback).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not affect other pending saves when canceling one', () => {
|
||||
autoSaveManager.notifyChange('post-1', { content: 'Content 1' });
|
||||
autoSaveManager.notifyChange('post-2', { content: 'Content 2' });
|
||||
|
||||
autoSaveManager.cancel('post-1');
|
||||
|
||||
vi.advanceTimersByTime(5000);
|
||||
|
||||
expect(onSaveCallback).toHaveBeenCalledTimes(1);
|
||||
expect(onSaveCallback).toHaveBeenCalledWith('post-2', { content: 'Content 2' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Callbacks', () => {
|
||||
it('should call onSaveComplete after successful save', async () => {
|
||||
autoSaveManager.notifyChange('post-1', { content: 'Content' });
|
||||
|
||||
vi.advanceTimersByTime(3500);
|
||||
|
||||
// Wait for async save to complete
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(onSaveCompleteCallback).toHaveBeenCalledWith('post-1');
|
||||
});
|
||||
|
||||
it('should call onSaveError when save fails', async () => {
|
||||
const error = new Error('Save failed');
|
||||
onSaveCallback.mockRejectedValueOnce(error);
|
||||
|
||||
autoSaveManager.notifyChange('post-1', { content: 'Content' });
|
||||
|
||||
vi.advanceTimersByTime(3500);
|
||||
|
||||
// Wait for async save to complete
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(onSaveErrorCallback).toHaveBeenCalledWith('post-1', error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Has Pending Changes', () => {
|
||||
it('should report pending changes correctly', () => {
|
||||
expect(autoSaveManager.hasPendingChanges()).toBe(false);
|
||||
|
||||
autoSaveManager.notifyChange('post-1', { content: 'Content' });
|
||||
|
||||
expect(autoSaveManager.hasPendingChanges()).toBe(true);
|
||||
expect(autoSaveManager.hasPendingChanges('post-1')).toBe(true);
|
||||
expect(autoSaveManager.hasPendingChanges('post-2')).toBe(false);
|
||||
});
|
||||
|
||||
it('should clear pending status after save', async () => {
|
||||
autoSaveManager.notifyChange('post-1', { content: 'Content' });
|
||||
|
||||
vi.advanceTimersByTime(3500);
|
||||
await vi.runAllTimersAsync();
|
||||
|
||||
expect(autoSaveManager.hasPendingChanges('post-1')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dispose', () => {
|
||||
it('should cancel all pending saves on dispose', () => {
|
||||
autoSaveManager.notifyChange('post-1', { content: 'Content' });
|
||||
|
||||
autoSaveManager.dispose();
|
||||
|
||||
vi.advanceTimersByTime(5000);
|
||||
|
||||
expect(onSaveCallback).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Configuration', () => {
|
||||
it('should use custom idle time', () => {
|
||||
autoSaveManager.dispose();
|
||||
|
||||
autoSaveManager = new AutoSaveManager({
|
||||
idleTimeMs: 5000, // 5 seconds
|
||||
onSave: onSaveCallback,
|
||||
onSaveComplete: onSaveCompleteCallback,
|
||||
onSaveError: onSaveErrorCallback,
|
||||
});
|
||||
|
||||
autoSaveManager.notifyChange('post-1', { content: 'Content' });
|
||||
|
||||
// Should not save at 4 seconds
|
||||
vi.advanceTimersByTime(4000);
|
||||
expect(onSaveCallback).not.toHaveBeenCalled();
|
||||
|
||||
// Should save at 5.5 seconds
|
||||
vi.advanceTimersByTime(1500);
|
||||
expect(onSaveCallback).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user