diff --git a/packages/plugin-dev/sync-md/src/background/__tests__/sync/sync-manager-debounce.test.ts b/packages/plugin-dev/sync-md/src/background/__tests__/sync/sync-manager-debounce.test.ts index c1f43db375..d5f7bf36e5 100644 --- a/packages/plugin-dev/sync-md/src/background/__tests__/sync/sync-manager-debounce.test.ts +++ b/packages/plugin-dev/sync-md/src/background/__tests__/sync/sync-manager-debounce.test.ts @@ -87,7 +87,7 @@ describe('Sync Manager Debounce Behavior', () => { }); describe('MD to SP sync debouncing', () => { - it('should debounce MD to SP sync for 10 seconds', async () => { + it('should sync immediately when window is focused', async () => { // Initialize sync manager - it will create file since getFileStats returns null initSyncManager(mockConfig); @@ -97,6 +97,30 @@ describe('Sync Manager Debounce Behavior', () => { // Clear mocks after initial sync jest.clearAllMocks(); + // Window is focused by default + // Trigger file change + mockFileChangeCallback(); + + // Should sync immediately without waiting + await jest.runAllTimersAsync(); + + expect(mdToSp).toHaveBeenCalledTimes(1); + expect(mdToSp).toHaveBeenCalledWith('- [ ] Test task', mockConfig.projectId); + }); + + it('should debounce MD to SP sync for 10 seconds when window is not focused', async () => { + // Initialize sync manager + initSyncManager(mockConfig); + + // Let initial sync complete + await jest.runAllTimersAsync(); + + // Clear mocks after initial sync + jest.clearAllMocks(); + + // Set window as unfocused + mockWindowFocusCallback(false); + // Trigger file change mockFileChangeCallback(); @@ -118,11 +142,35 @@ describe('Sync Manager Debounce Behavior', () => { expect(mdToSp).toHaveBeenCalledWith('- [ ] Test task', mockConfig.projectId); }); - it('should reset debounce timer on multiple file changes', async () => { + it('should handle multiple file changes when window is focused', async () => { initSyncManager(mockConfig); await jest.runAllTimersAsync(); jest.clearAllMocks(); + // Window is focused - each file change triggers immediate sync + + // First file change + mockFileChangeCallback(); + await jest.runAllTimersAsync(); + + expect(mdToSp).toHaveBeenCalledTimes(1); + + // Second file change + mockFileChangeCallback(); + await jest.runAllTimersAsync(); + + // Should be called again (not debounced when focused) + expect(mdToSp).toHaveBeenCalledTimes(2); + }); + + it('should reset debounce timer on multiple file changes when unfocused', async () => { + initSyncManager(mockConfig); + await jest.runAllTimersAsync(); + jest.clearAllMocks(); + + // Set window as unfocused + mockWindowFocusCallback(false); + // First file change mockFileChangeCallback(); @@ -144,7 +192,7 @@ describe('Sync Manager Debounce Behavior', () => { // Run all timers and wait for async operations await jest.runAllTimersAsync(); - // Now sync should be called + // Now sync should be called only once expect(mdToSp).toHaveBeenCalledTimes(1); }); @@ -202,7 +250,10 @@ describe('Sync Manager Debounce Behavior', () => { await jest.runAllTimersAsync(); jest.clearAllMocks(); - // Trigger file change + // Set window as unfocused first + mockWindowFocusCallback(false); + + // Trigger file change while unfocused mockFileChangeCallback(); // Fast forward to complete the sync @@ -213,8 +264,7 @@ describe('Sync Manager Debounce Behavior', () => { expect(mdToSp).toHaveBeenCalledTimes(1); - // Window loses focus then gains focus - mockWindowFocusCallback(false); + // Window gains focus after sync completed mockWindowFocusCallback(true); await jest.runAllTimersAsync(); @@ -228,15 +278,12 @@ describe('Sync Manager Debounce Behavior', () => { await jest.runAllTimersAsync(); jest.clearAllMocks(); - // Window starts focused - mockWindowFocusCallback(true); - - // Trigger file change - mockFileChangeCallback(); - - // Window loses focus + // Start with window unfocused mockWindowFocusCallback(false); + // Trigger file change while unfocused + mockFileChangeCallback(); + // Fast forward 5 seconds (half of debounce time) jest.advanceTimersByTime(5000); expect(mdToSp).not.toHaveBeenCalled(); diff --git a/packages/plugin-dev/sync-md/src/background/sync/sync-manager.ts b/packages/plugin-dev/sync-md/src/background/sync/sync-manager.ts index 4aedec1d8f..80655a72d8 100644 --- a/packages/plugin-dev/sync-md/src/background/sync/sync-manager.ts +++ b/packages/plugin-dev/sync-md/src/background/sync/sync-manager.ts @@ -78,12 +78,21 @@ const handleFileChange = (config: LocalUserCfg): void => { clearTimeout(mdToSpDebounceTimer); } + // If window is focused, sync immediately without debounce + if (isWindowFocused) { + console.log( + '[sync-md] File change detected while window is focused, syncing immediately', + ); + handleMdToSpSync(config); + return; + } + // Mark that we have a pending MD to SP sync pendingMdToSpSync = config; - // Always use 10 second debounce for MD to SP sync + // Use 10 second debounce for MD to SP sync when window is not focused console.log( - `[sync-md] File change detected, debouncing for ${SYNC_DEBOUNCE_MS_MD_TO_SP}ms (10 seconds), window focused: ${isWindowFocused}`, + `[sync-md] File change detected while window is unfocused, debouncing for ${SYNC_DEBOUNCE_MS_MD_TO_SP}ms (10 seconds)`, ); mdToSpDebounceTimer = setTimeout(() => { @@ -208,6 +217,7 @@ const setupWindowFocusTracking = (): void => { // Trigger the sync immediately with the stored config const configToSync = pendingMdToSpSync; + pendingMdToSpSync = null; handleMdToSpSync(configToSync); } else { console.log(