super-productivity/e2e/tests/focus-mode/flowtime-timer-bug-5117.spec.ts
Johannes Millan b0f4e99c0b test(e2e): fix flaky focus mode tests
Fixed 6 flaky E2E tests in focus mode by addressing race conditions
with countdown animation and session state transitions.

Changes:
- Added pointer-events: none to countdown component to prevent blocking
  clicks during fade-out animation (195ms)
- Replaced arbitrary timeouts with explicit waits for session-in-progress
  indicator (complete session button) in all focus mode tests
- Tests now wait for countdown animation to fully complete before
  interacting with UI elements

Root causes:
1. Countdown overlay intercepted pointer events during fade animation,
   causing clicks to fail intermittently
2. 900ms delay between countdown completion and session start caused
   race conditions when using fixed timeouts

Affected tests:
- focus-mode-break.spec.ts (4 tests)
- flowtime-timer-bug-5117.spec.ts (2 tests)

All tests now pass consistently without retries.
2026-01-16 22:34:50 +01:00

298 lines
11 KiB
TypeScript

/**
* E2E test for GitHub issue #5117
* https://github.com/super-productivity/super-productivity/issues/5117
*
* Bug: Flowtime focus mode stops counting up at the value set in Countdown mode
* (e.g., 25 minutes or 5 minutes) instead of counting indefinitely.
*
* User reproduction steps:
* 1. Open focus mode
* 2. Switch to Countdown tab
* 3. Set countdown to 5 minutes (but do not start the session)
* 4. Switch to Flowtime tab
* 5. Start focus session counting up from 0
*
* Expected: Timer counts indefinitely
* Actual: Timer stops at 5 minutes (the Countdown value)
*/
import { test, expect } from '../../fixtures/test.fixture';
import { WorkViewPage } from '../../pages/work-view.page';
// Helper to parse time string "MM:SS" to seconds
const parseTime = (timeStr: string | null): number => {
if (!timeStr) return 0;
const trimmed = timeStr.trim();
const parts = trimmed.split(':');
if (parts.length !== 2) return 0;
const minutes = parseInt(parts[0], 10);
const seconds = parseInt(parts[1], 10);
const minutesInSeconds = minutes * 60;
return minutesInSeconds + seconds;
};
test.describe('Bug #5117: Flowtime timer stops at Countdown duration', () => {
test('Flowtime timer should count past the previously set Countdown duration', async ({
page,
testPrefix,
}) => {
const workViewPage = new WorkViewPage(page, testPrefix);
// Locators
const focusModeOverlay = page.locator('focus-mode-overlay');
const mainFocusButton = page
.getByRole('button')
.filter({ hasText: 'center_focus_strong' });
// Mode selector buttons - using the segmented button group
const flowtimeButton = page.locator('segmented-button-group button', {
hasText: 'Flowtime',
});
const countdownButton = page.locator('segmented-button-group button', {
hasText: 'Countdown',
});
// Duration slider (visible only in Countdown mode during preparation)
const durationSlider = page.locator('input-duration-slider');
// Play button to start the session (button element, not the icon inside)
const playButton = page.locator('focus-mode-main button.play-button');
// Clock display showing elapsed time
const clockTime = page.locator('focus-mode-main .clock-time');
// Wait for task list and add a task (required for focus mode)
await workViewPage.waitForTaskList();
await workViewPage.addTask('FlowTimeTestTask');
// Step 1: Open focus mode
await mainFocusButton.click();
await expect(focusModeOverlay).toBeVisible({ timeout: 5000 });
// Focus mode may show task selection placeholder first - select the task
const taskSelectionPlaceholder = page.locator('.task-title-placeholder');
const isPlaceholderVisible = await taskSelectionPlaceholder
.isVisible()
.catch(() => false);
if (isPlaceholderVisible) {
console.log('Task selection placeholder visible, selecting task...');
// Click on the placeholder to open task selector
await taskSelectionPlaceholder.click();
// Wait for task selector overlay
const taskSelectorOverlay = page.locator('.task-selector-overlay');
await expect(taskSelectorOverlay).toBeVisible({ timeout: 3000 });
// Click on the first suggested task (mat-option is in CDK overlay panel)
const suggestedTask = page.locator('mat-option, .mat-mdc-option').first();
await expect(suggestedTask).toBeVisible({ timeout: 5000 });
await suggestedTask.click();
// Wait for task selector overlay to close
await expect(taskSelectorOverlay).not.toBeVisible({ timeout: 5000 });
}
// Step 2: Switch to Countdown mode
await countdownButton.click();
await expect(countdownButton).toHaveClass(/is-active/, { timeout: 2000 });
// Step 3: The slider should be visible in Countdown mode during preparation
await expect(durationSlider).toBeVisible({ timeout: 3000 });
// Get initial timer state before any changes
const initialClockText = await clockTime.textContent();
console.log('Initial clock text in Countdown mode:', initialClockText);
// Step 4: Switch to Flowtime mode
await flowtimeButton.click();
await expect(flowtimeButton).toHaveClass(/is-active/, { timeout: 2000 });
// Duration slider should NOT be visible in Flowtime mode
await expect(durationSlider).not.toBeVisible({ timeout: 2000 });
// Verify clock shows 0:00 (Flowtime starts at 0 and counts up)
await expect(clockTime).toHaveText('0:00', { timeout: 3000 });
// Step 5: Start the focus session by clicking play button
await expect(playButton).toBeVisible({ timeout: 2000 });
await playButton.click();
// Wait for the 5-4-3-2-1 countdown animation to complete
const countdownComponent = page.locator('focus-mode-countdown');
const completeSessionButton = page.locator(
'focus-mode-main button.complete-session-btn',
);
// Wait for countdown to appear and then disappear
try {
await expect(countdownComponent).toBeVisible({ timeout: 2000 });
console.log('Countdown animation started...');
// Wait for countdown to complete (5 seconds + animation buffer)
await expect(countdownComponent).not.toBeVisible({ timeout: 15000 });
console.log('Countdown animation completed');
} catch {
console.log('Countdown animation not visible (may be skipped in settings)');
}
// Wait for session to be in progress (complete session button becomes visible)
await expect(completeSessionButton).toBeVisible({ timeout: 10000 });
// Wait for clock-time to show a non-zero value (indicating timer has started ticking)
await expect(async () => {
const text = await clockTime.textContent();
const trimmed = text?.trim() || '';
console.log('Current clock time:', trimmed);
const seconds = parseTime(trimmed);
expect(seconds).toBeGreaterThan(0);
}).toPass({ timeout: 10000 });
// Now verify the timer continues to increase
const time1 = await clockTime.textContent();
console.log('Time at T1:', time1);
await page.waitForTimeout(3000);
const time2 = await clockTime.textContent();
console.log('Time at T2:', time2);
const seconds1 = parseTime(time1);
const seconds2 = parseTime(time2);
console.log('Seconds at T1:', seconds1);
console.log('Seconds at T2:', seconds2);
// Timer should have increased
expect(seconds2).toBeGreaterThan(seconds1);
// Verify once more
await page.waitForTimeout(2000);
const time3 = await clockTime.textContent();
const seconds3 = parseTime(time3);
console.log('Seconds at T3:', seconds3);
expect(seconds3).toBeGreaterThan(seconds2);
});
test('Exact bug scenario: Set Countdown duration, switch to Flowtime, verify timer runs', async ({
page,
testPrefix,
}) => {
// This is the exact user scenario from bug report
const workViewPage = new WorkViewPage(page, testPrefix);
const focusModeOverlay = page.locator('focus-mode-overlay');
const mainFocusButton = page
.getByRole('button')
.filter({ hasText: 'center_focus_strong' });
const flowtimeButton = page.locator('segmented-button-group button', {
hasText: 'Flowtime',
});
const countdownButton = page.locator('segmented-button-group button', {
hasText: 'Countdown',
});
const playButton = page.locator('focus-mode-main button.play-button');
const clockTime = page.locator('focus-mode-main .clock-time');
const durationSlider = page.locator('input-duration-slider');
// Setup
await workViewPage.waitForTaskList();
await workViewPage.addTask('FlowTimeTestTask2');
// Open focus mode
await mainFocusButton.click();
await expect(focusModeOverlay).toBeVisible({ timeout: 5000 });
// Focus mode may show task selection placeholder first - select the task
const taskSelectionPlaceholder = page.locator('.task-title-placeholder');
const isPlaceholderVisible = await taskSelectionPlaceholder
.isVisible()
.catch(() => false);
if (isPlaceholderVisible) {
console.log('Task selection placeholder visible, selecting task...');
await taskSelectionPlaceholder.click();
const taskSelectorOverlay = page.locator('.task-selector-overlay');
await expect(taskSelectorOverlay).toBeVisible({ timeout: 3000 });
const suggestedTask = page.locator('mat-option, .mat-mdc-option').first();
await expect(suggestedTask).toBeVisible({ timeout: 5000 });
await suggestedTask.click();
// Wait for task selector overlay to close
await expect(taskSelectorOverlay).not.toBeVisible({ timeout: 5000 });
}
// Step 1: Switch to Countdown mode
await countdownButton.click();
await expect(countdownButton).toHaveClass(/is-active/, { timeout: 2000 });
await expect(durationSlider).toBeVisible({ timeout: 2000 });
// Step 2: Note the countdown duration displayed (default 25 min or whatever is set)
const countdownTime = await clockTime.textContent();
console.log('Countdown duration displayed:', countdownTime);
// Step 3: Switch to Flowtime (without starting the countdown session)
await flowtimeButton.click();
await expect(flowtimeButton).toHaveClass(/is-active/, { timeout: 2000 });
// Clock should show 0:00 for Flowtime
await expect(clockTime).toHaveText('0:00', { timeout: 3000 });
// Step 4: Start the Flowtime session
await expect(playButton).toBeVisible({ timeout: 2000 });
await playButton.click();
// Wait for countdown animation to complete
const countdownComponent = page.locator('focus-mode-countdown');
const completeSessionButton = page.locator(
'focus-mode-main button.complete-session-btn',
);
try {
await expect(countdownComponent).toBeVisible({ timeout: 2000 });
console.log('Countdown animation started...');
await expect(countdownComponent).not.toBeVisible({ timeout: 15000 });
console.log('Countdown animation completed');
} catch {
console.log('Countdown animation not visible (may be skipped)');
}
// Wait for session to be in progress (complete session button becomes visible)
await expect(completeSessionButton).toBeVisible({ timeout: 10000 });
// Wait for clock-time to show a non-zero value
await expect(async () => {
const text = await clockTime.textContent();
const trimmed = text?.trim() || '';
console.log('Current clock time:', trimmed);
const seconds = parseTime(trimmed);
expect(seconds).toBeGreaterThan(0);
}).toPass({ timeout: 10000 });
// Step 5: Verify timer is counting up and doesn't stop
const time1 = await clockTime.textContent();
console.log('Flowtime time T1:', time1);
await page.waitForTimeout(3000);
const time2 = await clockTime.textContent();
console.log('Flowtime time T2:', time2);
await page.waitForTimeout(3000);
const time3 = await clockTime.textContent();
console.log('Flowtime time T3:', time3);
// Parse times
const seconds1 = parseTime(time1);
const seconds2 = parseTime(time2);
const seconds3 = parseTime(time3);
console.log('Seconds:', seconds1, '->', seconds2, '->', seconds3);
// All times should be increasing (timer is running)
expect(seconds2).toBeGreaterThan(seconds1);
expect(seconds3).toBeGreaterThan(seconds2);
// The timer should continue running indefinitely
// If bug is present, it would stop at the countdown duration
});
});