From 4f2e4b41ceaf883c2a07971910e1166fd22b187e Mon Sep 17 00:00:00 2001 From: Johannes Millan Date: Thu, 15 Jan 2026 12:59:50 +0100 Subject: [PATCH] fix(focus-mode): show overlay when no valid task exists for sync #5954 When starting a focus session with "Sync focus sessions with time tracking" enabled, if no valid (undone) task exists, dispatch showFocusOverlay so the user can select or create a task instead of running the timer untracked. --- .../pomodoro-timer-sync-bug-5954.spec.ts | 119 ++++++++++++++++++ .../store/focus-mode.effects.spec.ts | 39 ++---- .../focus-mode/store/focus-mode.effects.ts | 6 +- 3 files changed, 135 insertions(+), 29 deletions(-) diff --git a/e2e/tests/focus-mode/pomodoro-timer-sync-bug-5954.spec.ts b/e2e/tests/focus-mode/pomodoro-timer-sync-bug-5954.spec.ts index 4274118c1..83940409d 100644 --- a/e2e/tests/focus-mode/pomodoro-timer-sync-bug-5954.spec.ts +++ b/e2e/tests/focus-mode/pomodoro-timer-sync-bug-5954.spec.ts @@ -251,4 +251,123 @@ test.describe('Bug #5954: Pomodoro timer sync issues', () => { } }); }); + + test.describe('No valid task available (Bug #5954 comment)', () => { + /** + * Tests for the scenario where user starts focus mode but all tasks are done. + * The fix ensures the focus overlay appears so user can select/create a task. + * https://github.com/super-productivity/super-productivity/issues/5954#issuecomment-3753395324 + */ + test('should keep overlay visible when starting session with all tasks done', async ({ + page, + testPrefix, + taskPage, + }) => { + const workViewPage = new WorkViewPage(page, testPrefix); + const focusModeOverlay = page.locator('focus-mode-overlay'); + const mainFocusButton = page + .getByRole('button') + .filter({ hasText: 'center_focus_strong' }); + + // Step 1: Create a task and mark it as done immediately + await workViewPage.waitForTaskList(); + await workViewPage.addTask('CompletedTaskTest'); + + const task = page.locator('task').first(); + await expect(task).toBeVisible(); + + // Mark task as done + await taskPage.markTaskAsDone(task); + await expect(task).toHaveClass(/isDone/, { timeout: 5000 }); + + // Step 2: Open focus mode (no task is being tracked) + await mainFocusButton.click(); + await expect(focusModeOverlay).toBeVisible({ timeout: 5000 }); + + // Step 3: Select Pomodoro mode and start session + await selectPomodoroMode(page); + + const playButton = page.locator('focus-mode-main button.play-button'); + await expect(playButton).toBeVisible({ timeout: 2000 }); + await playButton.click(); + + // Wait for any countdown to complete + const countdownComponent = page.locator('focus-mode-countdown'); + try { + const isVisible = await countdownComponent.isVisible().catch(() => false); + if (isVisible) { + await expect(countdownComponent).not.toBeVisible({ timeout: 15000 }); + } + } catch { + // Countdown may be skipped + } + + // Step 4: Verify the overlay remains visible (fix for bug #5954) + // The showFocusOverlay action should be dispatched when no valid task exists + await expect(focusModeOverlay).toBeVisible({ timeout: 5000 }); + + // Session should be in progress (timer running) + const completeSessionBtn = page.locator('focus-mode-main .complete-session-btn'); + await expect(completeSessionBtn).toBeVisible({ timeout: 10000 }); + }); + + test('should keep overlay visible when last tracked task was completed', async ({ + page, + testPrefix, + taskPage, + }) => { + const workViewPage = new WorkViewPage(page, testPrefix); + const focusModeOverlay = page.locator('focus-mode-overlay'); + const mainFocusButton = page + .getByRole('button') + .filter({ hasText: 'center_focus_strong' }); + + // Step 1: Create task and start tracking + await workViewPage.waitForTaskList(); + await workViewPage.addTask('TrackThenCompleteTest'); + + const task = page.locator('task').first(); + await expect(task).toBeVisible(); + + // Start tracking the task + await task.hover(); + const playButton = page.locator('.play-btn.tour-playBtn').first(); + await playButton.waitFor({ state: 'visible' }); + await playButton.click(); + await expect(task).toHaveClass(/isCurrent/, { timeout: 5000 }); + + // Step 2: Mark task as done (this stops tracking) + await taskPage.markTaskAsDone(task); + await expect(task).toHaveClass(/isDone/, { timeout: 5000 }); + await expect(task).not.toHaveClass(/isCurrent/, { timeout: 5000 }); + + // Step 3: Open focus mode and try to start session + await mainFocusButton.click(); + await expect(focusModeOverlay).toBeVisible({ timeout: 5000 }); + + await selectPomodoroMode(page); + + const sessionPlayButton = page.locator('focus-mode-main button.play-button'); + await expect(sessionPlayButton).toBeVisible({ timeout: 2000 }); + await sessionPlayButton.click(); + + // Wait for countdown + const countdownComponent = page.locator('focus-mode-countdown'); + try { + const isVisible = await countdownComponent.isVisible().catch(() => false); + if (isVisible) { + await expect(countdownComponent).not.toBeVisible({ timeout: 15000 }); + } + } catch { + // Countdown may be skipped + } + + // Step 4: Verify overlay stays visible for task selection + await expect(focusModeOverlay).toBeVisible({ timeout: 5000 }); + + // Session should still start (timer runs, user can select task from overlay) + const completeSessionBtn = page.locator('focus-mode-main .complete-session-btn'); + await expect(completeSessionBtn).toBeVisible({ timeout: 10000 }); + }); + }); }); diff --git a/src/app/features/focus-mode/store/focus-mode.effects.spec.ts b/src/app/features/focus-mode/store/focus-mode.effects.spec.ts index ea709e066..ae0e8e541 100644 --- a/src/app/features/focus-mode/store/focus-mode.effects.spec.ts +++ b/src/app/features/focus-mode/store/focus-mode.effects.spec.ts @@ -1782,7 +1782,7 @@ describe('FocusModeEffects', () => { }, 50); }); - it('should NOT dispatch setCurrentTask when task no longer exists', (done) => { + it('should dispatch showFocusOverlay when task no longer exists (Bug #5954)', (done) => { store.overrideSelector(selectFocusModeConfig, { isSyncSessionWithTracking: true, isSkipPreparation: false, @@ -1796,15 +1796,10 @@ describe('FocusModeEffects', () => { actions$ = of(actions.startFocusSession({ duration: 25 * 60 * 1000 })); - let emitted = false; - effects.syncSessionStartToTracking$.subscribe(() => { - emitted = true; - }); - - setTimeout(() => { - expect(emitted).toBe(false); + effects.syncSessionStartToTracking$.subscribe((action) => { + expect(action.type).toEqual('[FocusMode] Show Overlay'); done(); - }, 50); + }); }); it('should fall back to lastCurrentTask when no pausedTaskId (Bug #5954)', (done) => { @@ -1836,7 +1831,7 @@ describe('FocusModeEffects', () => { }); }); - it('should NOT dispatch when lastCurrentTask is done', (done) => { + it('should dispatch showFocusOverlay when lastCurrentTask is done (Bug #5954)', (done) => { store.overrideSelector(selectFocusModeConfig, { isSyncSessionWithTracking: true, isSkipPreparation: false, @@ -1858,15 +1853,10 @@ describe('FocusModeEffects', () => { actions$ = of(actions.startFocusSession({ duration: 25 * 60 * 1000 })); - let emitted = false; - effects.syncSessionStartToTracking$.subscribe(() => { - emitted = true; - }); - - setTimeout(() => { - expect(emitted).toBe(false); + effects.syncSessionStartToTracking$.subscribe((action) => { + expect(action.type).toEqual('[FocusMode] Show Overlay'); done(); - }, 50); + }); }); }); @@ -2642,7 +2632,7 @@ describe('FocusModeEffects', () => { }); }); - it('should NOT dispatch when lastCurrentTask no longer exists in store', (done) => { + it('should dispatch showFocusOverlay when lastCurrentTask no longer exists in store (Bug #5954)', (done) => { store.overrideSelector(selectFocusModeConfig, { isSyncSessionWithTracking: true, isSkipPreparation: false, @@ -2660,15 +2650,10 @@ describe('FocusModeEffects', () => { actions$ = of(actions.startFocusSession({ duration: 25 * 60 * 1000 })); - let emitted = false; - effects.syncSessionStartToTracking$.subscribe(() => { - emitted = true; - }); - - setTimeout(() => { - expect(emitted).toBe(false); + effects.syncSessionStartToTracking$.subscribe((action) => { + expect(action.type).toEqual('[FocusMode] Show Overlay'); done(); - }, 50); + }); }); }); diff --git a/src/app/features/focus-mode/store/focus-mode.effects.ts b/src/app/features/focus-mode/store/focus-mode.effects.ts index 36c7dd4fc..05b72cda9 100644 --- a/src/app/features/focus-mode/store/focus-mode.effects.ts +++ b/src/app/features/focus-mode/store/focus-mode.effects.ts @@ -201,6 +201,7 @@ export class FocusModeEffects { // Sync: When focus session starts → start tracking (if not already tracking) // Checks that the paused task still exists before starting tracking // Bug #5954 fix: Falls back to lastCurrentTask if no pausedTaskId (e.g., after app restart) + // Bug #5954 fix: Shows focus overlay if no valid (undone) task is available syncSessionStartToTracking$ = createEffect(() => this.actions$.pipe( ofType(actions.startFocusSession), @@ -224,11 +225,12 @@ export class FocusModeEffects { return this.store.select(selectTaskById, { id: taskIdToResume }).pipe( take(1), map((task) => - task && !task.isDone ? setCurrentTask({ id: taskIdToResume }) : null, + task && !task.isDone + ? setCurrentTask({ id: taskIdToResume }) + : actions.showFocusOverlay(), ), ); }), - filter((action): action is ReturnType => action !== null), ), );