diff --git a/.backlog/tasks/doro-010 - Add-possibility-to-adjust-long-short-work-time-on-UI.md b/.backlog/tasks/doro-010 - Add-possibility-to-adjust-long-short-work-time-on-UI.md new file mode 100644 index 0000000..f7b2f3e --- /dev/null +++ b/.backlog/tasks/doro-010 - Add-possibility-to-adjust-long-short-work-time-on-UI.md @@ -0,0 +1,43 @@ +--- +id: DORO-010 +title: Add possibility to adjust long/short/work time on UI +status: In Progress +assignee: + - '@antigravity' +created_date: '2026-05-15 10:29' +updated_date: '2026-05-25 19:12' +labels: [] +dependencies: [] +--- + +## Description + + + +Allow users to adjust the duration (in minutes) for short breaks, long breaks, and work sessions directly from the UI (including tiny mode). + +- Short break: 3-7 mins +- Long break: 10-18 mins +- Work: 20-30 mins +- Save these settings into the existing config. + + +## Implementation Plan + + + +1. Extract MODE_DURATION_BOUNDS and EDIT_SAVE_TIMEOUT_MS constants to config.ts +2. Update handleDurationEdit in app.ts to clamp the initial value and use the new constants +3. Correct // no latestVersion comment placement in ui.test.ts +4. Run lint, typecheck, and unit tests +5. Update visual regression test snapshots to ensure all tests pass + + +## Definition of Done + + + +- [ ] #1 code coverage is passing +- [ ] #2 VRTs added for ui changes +- [ ] #3 tests/linting/typecheck is green. + diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..cf45a49 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,8 @@ +coverage: + status: + project: + default: + target: 98% + patch: + default: + target: 98% diff --git a/eslint.config.mjs b/eslint.config.mjs index 0b05a36..a04cf76 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -31,6 +31,7 @@ export default tseslint.config( 'dist/**', 'coverage/**', 'node_modules/**', + '.pi/**', 'playwright.config.ts', 'vitest.config.ts', '.pi/**' diff --git a/package.json b/package.json index 5b060a9..030f561 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "maintained node versions" ], "lint-staged": { - "src/**/*.{ts,tsx}": [ + "{src,tests}/**/*.{ts,tsx,mts}": [ "eslint --fix", "prettier --write" ] diff --git a/src/__tests__/app.test.ts b/src/__tests__/app.test.ts index edf0887..5d198f7 100644 --- a/src/__tests__/app.test.ts +++ b/src/__tests__/app.test.ts @@ -66,7 +66,8 @@ describe('DoroApp', () => { toggleLock: vi.fn(), togglePause: vi.fn(), debugJumpToNearEnd: vi.fn(), - resetCurrentAndRun: vi.fn() + resetCurrentAndRun: vi.fn(), + updateConfig: vi.fn() // Add other methods of TimerStateMachine as they are used } as unknown as Mocked; @@ -432,6 +433,53 @@ describe('DoroApp', () => { expect(playClip).toHaveBeenCalled(); }); + it('should bail handleDurationEdit if value is null', () => { + app.start(); + (app as any).handleDurationEdit('increaseDuration'); // Start edit + (app as any).editDurationValue = null; // force null for test + (app as any).handleDurationEdit('increaseDuration'); // should early return + expect((app as any).editDurationValue).toBeNull(); + }); + + it('should edit duration and cancel edit if invalid', () => { + app.start(); + (app as any).handleDurationEdit('increaseDuration'); // Start edit + expect((app as any).editDurationState).toBe('editing'); + + (app as any).editDurationValue = null; + (app as any).saveDurationEdit(); // Submit invalid + + // With invalid / null editDurationValue it should cancel + expect((app as any).editDurationState).toBe('none'); + }); + + it('should edit duration and apply short mode', () => { + app.start(); + mockTimerStateMachine.getState.mockReturnValue({ mode: 'short' } as any); + + (app as any).handleDurationEdit('increaseDuration'); // Start edit + expect((app as any).editDurationState).toBe('editing'); + + (app as any).editDurationValue = 5; + (app as any).saveDurationEdit(); // Submit + + expect(mockTimerStateMachine.updateConfig).toHaveBeenCalled(); + }); + + it('should edit duration and apply long mode', () => { + app.start(); + mockTimerStateMachine.getState.mockReturnValue({ mode: 'long' } as any); + + (app as any).handleDurationEdit('increaseDuration'); // Start edit + expect((app as any).editDurationState).toBe('editing'); + (app as any).handleDurationEdit('decreaseDuration'); // Trigger line 424 + + (app as any).editDurationValue = 15; + (app as any).saveDurationEdit(); // Submit + + expect(mockTimerStateMachine.updateConfig).toHaveBeenCalled(); + }); + it('should play completion and reset beeps', () => { // Test completion beep mockTimerStateMachine.getState.mockReturnValue({ status: 'running' } as any); @@ -569,6 +617,189 @@ describe('DoroApp', () => { }); describe('additional input handling', () => { + it('should enter edit duration mode and toggle blink', () => { + // Mock basic state + mockTimerStateMachine.getState.mockReturnValue({ + mode: 'work', + status: 'running', + remainingSeconds: 1200, + isLocked: false, + switchPrompt: null, + completedWorkSessions: 1 + }); + mockTimerStateMachine.getConfig.mockReturnValue({ + workSeconds: 22 * 60, + shortRestSeconds: 5 * 60, + longRestSeconds: 12 * 60, + longRestEveryWorkSessions: 4, + switchConfirmSeconds: 300, + audioVolume: 0.5, + theme: 'modern', + tickSoundEnabled: true, + mascotVariant: 'default' + } as any); + + const mockRender = vi.spyOn(mockDoroUi, 'render'); + + // 1. Initial increase to start editing + vi.mocked(resolveControlCommand).mockReturnValue('increaseDuration'); + (app as any).handleInput({ + type: 'key', + ch: '+', + keyName: '+', + keyFull: '+', + shift: false, + ctrl: false + }); + + expect((app as any).editDurationState).toBe('editing'); + expect((app as any).editDurationValue).toBe(22); + expect(mockRender).toHaveBeenCalled(); + + // 2. Second increase to actually change value + (app as any).handleInput({ + type: 'key', + ch: '+', + keyName: '+', + keyFull: '+', + shift: false, + ctrl: false + }); + + expect((app as any).editDurationValue).toBe(23); + + // 3. Decrease value + vi.mocked(resolveControlCommand).mockReturnValue('decreaseDuration'); + (app as any).handleInput({ + type: 'key', + ch: '-', + keyName: '-', + keyFull: '-', + shift: false, + ctrl: false + }); + + expect((app as any).editDurationValue).toBe(22); + + // 4. Tick should toggle blink every 3 ticks + // Initialize internal counter so first tick doesn't immediately false out + (app as any).blinkCounter = 0; + (app as any).editDurationBlink = false; + + // Fast forward 250ms ticks to trigger blink toggle + vi.useFakeTimers(); + (app as any).start(); // Need to start the app to get the setInterval + vi.advanceTimersByTime(250); // count 1 (blink = false) + vi.advanceTimersByTime(250); // count 2 (blink = false) + vi.advanceTimersByTime(250); // count 3 - should toggle + expect((app as any).editDurationBlink).toBe(true); + }); + + it('should save duration on timeout', () => { + vi.useFakeTimers(); + + // Mock state and enter edit mode + mockTimerStateMachine.getState.mockReturnValue({ + mode: 'work', + status: 'running', + remainingSeconds: 1200, + isLocked: false, + switchPrompt: null, + completedWorkSessions: 1 + }); + mockTimerStateMachine.getConfig.mockReturnValue({ + workSeconds: 22 * 60, + shortRestSeconds: 5 * 60, + longRestSeconds: 12 * 60, + longRestEveryWorkSessions: 4, + switchConfirmSeconds: 300, + audioVolume: 0.5, + theme: 'modern', + tickSoundEnabled: true, + mascotVariant: 'default' + } as any); + + vi.mocked(resolveControlCommand).mockReturnValue('increaseDuration'); + (app as any).handleInput({ type: 'key', keyName: '+' }); + (app as any).handleInput({ type: 'key', keyName: '+' }); // value = 23 + + expect((app as any).editDurationState).toBe('editing'); + + // Fast forward 2 seconds to trigger save timeout + vi.advanceTimersByTime(2000); + + expect((app as any).editDurationState).toBe('saved'); + expect(mockTimerStateMachine.updateConfig).toHaveBeenCalledWith( + expect.objectContaining({ workSeconds: 23 * 60 }) + ); + + // Fast forward another 2 seconds to trigger clear timeout + vi.advanceTimersByTime(2000); + + expect((app as any).editDurationState).toBe('none'); + expect((app as any).editDurationValue).toBeNull(); + }); + + it('should clamp duration values', () => { + mockTimerStateMachine.getState.mockReturnValue({ + mode: 'work', + status: 'running', + remainingSeconds: 1200, + isLocked: false, + switchPrompt: null, + completedWorkSessions: 1 + }); + mockTimerStateMachine.getConfig.mockReturnValue({ + workSeconds: 30 * 60, // max work time + shortRestSeconds: 5 * 60, + longRestSeconds: 12 * 60, + longRestEveryWorkSessions: 4, + switchConfirmSeconds: 300, + audioVolume: 0.5, + theme: 'modern', + tickSoundEnabled: true, + mascotVariant: 'default' + } as any); + + // Start editing + vi.mocked(getDurationForMode).mockReturnValue(30 * 60); + vi.mocked(resolveControlCommand).mockReturnValue('increaseDuration'); + (app as any).handleInput({ type: 'key', keyName: '+' }); // Sets initial to 30 + + // Try to exceed max + (app as any).handleInput({ type: 'key', keyName: '+' }); + expect((app as any).editDurationValue).toBe(30); // should clamp to 30 + + // Reset test state for min clamp + mockTimerStateMachine.getState.mockReturnValue({ + mode: 'short', + status: 'running', + remainingSeconds: 300, + isLocked: false, + switchPrompt: null, + completedWorkSessions: 1 + }); + mockTimerStateMachine.getConfig.mockReturnValue({ + workSeconds: 22 * 60, + shortRestSeconds: 3 * 60, // min short rest time + longRestSeconds: 12 * 60, + longRestEveryWorkSessions: 4, + switchConfirmSeconds: 300, + audioVolume: 0.5, + theme: 'modern', + tickSoundEnabled: true, + mascotVariant: 'default' + } as any); + + (app as any).editDurationState = 'none'; + vi.mocked(getDurationForMode).mockReturnValue(3 * 60); + vi.mocked(resolveControlCommand).mockReturnValue('decreaseDuration'); + (app as any).handleInput({ type: 'key', keyName: '-' }); // Start edit mode + (app as any).handleInput({ type: 'key', keyName: '-' }); // Try to go below min + + expect((app as any).editDurationValue).toBe(3); // should clamp to 3 + }); + it('should handle quit command', () => { const mockShutdown = vi.spyOn(app as any, 'shutdown').mockImplementation(() => {}); vi.mocked(resolveControlCommand).mockReturnValue('quit'); diff --git a/src/__tests__/config.test.ts b/src/__tests__/config.test.ts index fa3e91e..4a95c60 100644 --- a/src/__tests__/config.test.ts +++ b/src/__tests__/config.test.ts @@ -39,7 +39,10 @@ describe('config', () => { expect(settings).toEqual({ volumeMode: 'normal', colorScheme: 'modern', - checkIntervalHours: 24 + checkIntervalHours: 24, + workDuration: 22, + shortBreakDuration: 5, + longBreakDuration: 12 }); }); @@ -52,7 +55,10 @@ describe('config', () => { expect(settings).toEqual({ volumeMode: 'quiet', colorScheme: 'calm', - checkIntervalHours: 24 + checkIntervalHours: 24, + workDuration: 22, + shortBreakDuration: 5, + longBreakDuration: 12 }); }); @@ -63,7 +69,10 @@ describe('config', () => { expect(settings).toEqual({ volumeMode: 'normal', colorScheme: 'modern', - checkIntervalHours: 24 + checkIntervalHours: 24, + workDuration: 22, + shortBreakDuration: 5, + longBreakDuration: 12 }); }); }); @@ -106,7 +115,10 @@ describe('config', () => { expect(settings).toEqual({ volumeMode: 'normal', colorScheme: 'modern', - checkIntervalHours: 24 + checkIntervalHours: 24, + workDuration: 22, + shortBreakDuration: 5, + longBreakDuration: 12 }); expect(fs.promises.writeFile).toHaveBeenCalled(); }); diff --git a/src/__tests__/input.test.ts b/src/__tests__/input.test.ts index 86f72b1..3dcd5b7 100644 --- a/src/__tests__/input.test.ts +++ b/src/__tests__/input.test.ts @@ -34,9 +34,21 @@ describe('input mapping', () => { expect(resolveControlCommand(createKeyEvent('N', 'n'))).toBe('updateNo'); }); + it('maps duration commands correctly', () => { + expect(resolveControlCommand(createKeyEvent('+', '+'))).toBe('increaseDuration'); + expect(resolveControlCommand(createKeyEvent('=', '='))).toBe('increaseDuration'); + expect(resolveControlCommand(createKeyEvent(undefined, 'up'))).toBe('increaseDuration'); + expect(resolveControlCommand(createKeyEvent(undefined, 'right'))).toBe('increaseDuration'); + + expect(resolveControlCommand(createKeyEvent('-', '-'))).toBe('decreaseDuration'); + expect(resolveControlCommand(createKeyEvent('_', '_'))).toBe('decreaseDuration'); + expect(resolveControlCommand(createKeyEvent(undefined, 'down'))).toBe('decreaseDuration'); + expect(resolveControlCommand(createKeyEvent(undefined, 'left'))).toBe('decreaseDuration'); + }); + it('does not crash when a key event arrives without a character', () => { expect(resolveControlCommand(createKeyEvent(undefined, 'q'))).toBe('quit'); - expect(resolveControlCommand(createKeyEvent(undefined, 'up'))).toBe('none'); + expect(resolveControlCommand(createKeyEvent(undefined, 'unknown'))).toBe('none'); }); it('allows only quit, pause, toggle lock, and update check when locked', () => { diff --git a/src/__tests__/stateMachine.test.ts b/src/__tests__/stateMachine.test.ts index 2e53f74..27416b7 100644 --- a/src/__tests__/stateMachine.test.ts +++ b/src/__tests__/stateMachine.test.ts @@ -307,6 +307,31 @@ describe('TimerStateMachine', () => { }); }); + it('should handle updateConfig with remaining seconds adjustment', () => { + const config = createQuickTestConfig({ workSeconds: 10 }); + const machine = new TimerStateMachine(config); + machine.startMode('work'); + + const newConfig = { ...config, workSeconds: config.workSeconds + 5 }; + machine.updateConfig(newConfig); + + const state = machine.getState(); + expect(state.remainingSeconds).toBe(15); + }); + + it('should handle updateConfig capping at 0', () => { + const config = createQuickTestConfig({ workSeconds: 10 }); + const machine = new TimerStateMachine(config); + machine.startMode('work'); + machine.tick(1000); // 1 sec + + const newConfig = { ...config, workSeconds: config.workSeconds - 20 }; + machine.updateConfig(newConfig); + + const state = machine.getState(); + expect(state.remainingSeconds).toBe(0); + }); + describe('confirmPromptAndSwitch edge cases', () => { it('returns null switchedToMode when no prompt exists', () => { // Arrange diff --git a/src/__tests__/ui.test.ts b/src/__tests__/ui.test.ts index 3009b51..d238a90 100644 --- a/src/__tests__/ui.test.ts +++ b/src/__tests__/ui.test.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { DoroUi, getRunningStatusText } from '../ui'; +import { DoroUi, getRunningStatusText, splitAtVisible } from '../ui'; import blessed from 'blessed'; import { enableMouse, disableMouse } from '../mouse'; @@ -39,6 +39,12 @@ describe('DoroUi', () => { }; let ui: DoroUi; + describe('Internal helpers', () => { + it('should split ansi string that is shorter than visible length', () => { + expect(splitAtVisible('hello', 10)).toEqual(['hello', '']); + }); + }); + beforeEach(() => { vi.clearAllMocks(); handlers = { @@ -84,7 +90,10 @@ describe('DoroUi', () => { promptTotalSeconds: 0, promptNextMode: null, updatePromptState: 'none', - updateCheckResult: null + updateCheckResult: null, + editDurationState: 'none', + editDurationValue: null, + editDurationBlink: false }); expect(mockScreen.render).toHaveBeenCalledTimes(1); @@ -106,11 +115,10 @@ describe('DoroUi', () => { promptTotalSeconds: 0, promptNextMode: null, updatePromptState: 'error', - updateCheckResult: { - isAvailable: false, - currentVersion: '1.2.1', - error: 'Network timeout' - } + updateCheckResult: { isAvailable: false, currentVersion: '1.2.1', error: 'Network timeout' }, + editDurationState: 'none', + editDurationValue: null, + editDurationBlink: false }); expect(mockScreen.render).toHaveBeenCalledTimes(1); @@ -132,11 +140,10 @@ describe('DoroUi', () => { promptTotalSeconds: 0, promptNextMode: null, updatePromptState: 'skipped', - updateCheckResult: { - isAvailable: true, - latestVersion: '1.3.0', - currentVersion: '1.2.1' - } + updateCheckResult: { isAvailable: true, latestVersion: '1.3.0', currentVersion: '1.2.1' }, + editDurationState: 'none', + editDurationValue: null, + editDurationBlink: false }); expect(mockScreen.render).toHaveBeenCalledTimes(1); @@ -158,7 +165,10 @@ describe('DoroUi', () => { promptTotalSeconds: 5, promptNextMode: 'short', updatePromptState: 'none', - updateCheckResult: null + updateCheckResult: null, + editDurationState: 'none', + editDurationValue: null, + editDurationBlink: false }); expect(mockScreen.render).toHaveBeenCalledTimes(1); @@ -181,7 +191,10 @@ describe('DoroUi', () => { promptTotalSeconds: 0, promptNextMode: null, updatePromptState: 'none', - updateCheckResult: null + updateCheckResult: null, + editDurationState: 'none', + editDurationValue: null, + editDurationBlink: false }); expect(mockScreen.render).toHaveBeenCalledTimes(1); @@ -204,7 +217,10 @@ describe('DoroUi', () => { promptTotalSeconds: 0, promptNextMode: null, updatePromptState: 'none', - updateCheckResult: null + updateCheckResult: null, + editDurationState: 'none', + editDurationValue: null, + editDurationBlink: false }); expect(mockScreen.render).toHaveBeenCalledTimes(1); @@ -226,7 +242,10 @@ describe('DoroUi', () => { promptTotalSeconds: 0, promptNextMode: null, updatePromptState: 'none', - updateCheckResult: null + updateCheckResult: null, + editDurationState: 'none', + editDurationValue: null, + editDurationBlink: false }); expect(mockScreen.render).toHaveBeenCalledTimes(1); @@ -297,11 +316,10 @@ describe('DoroUi', () => { promptTotalSeconds: 0, promptNextMode: null, updatePromptState: 'available', - updateCheckResult: { - isAvailable: true, - latestVersion: '1.3.0', - currentVersion: '1.2.1' - } + updateCheckResult: { isAvailable: true, latestVersion: '1.3.0', currentVersion: '1.2.1' }, + editDurationState: 'none', + editDurationValue: null, + editDurationBlink: false }); expect(mockScreen.render).toHaveBeenCalledTimes(1); @@ -322,7 +340,10 @@ describe('DoroUi', () => { promptTotalSeconds: 0, promptNextMode: null, updatePromptState: 'copySuccess', - updateCheckResult: null + updateCheckResult: null, + editDurationState: 'none', + editDurationValue: null, + editDurationBlink: false }); expect(mockScreen.render).toHaveBeenCalledTimes(1); @@ -342,7 +363,10 @@ describe('DoroUi', () => { promptTotalSeconds: 0, promptNextMode: null, updatePromptState: 'copyFallback', - updateCheckResult: null + updateCheckResult: null, + editDurationState: 'none', + editDurationValue: null, + editDurationBlink: false }); expect(mockScreen.render).toHaveBeenCalledTimes(1); @@ -362,10 +386,10 @@ describe('DoroUi', () => { promptTotalSeconds: 0, promptNextMode: null, updatePromptState: 'skipped', - updateCheckResult: { - isAvailable: false, - currentVersion: '1.2.1' - } + updateCheckResult: { isAvailable: false, currentVersion: '1.2.1' }, + editDurationState: 'none', + editDurationValue: null, + editDurationBlink: false }); expect(mockScreen.render).toHaveBeenCalledTimes(1); @@ -388,7 +412,10 @@ describe('DoroUi', () => { promptTotalSeconds: 0, promptNextMode: null, updatePromptState: 'none', - updateCheckResult: null + updateCheckResult: null, + editDurationState: 'none', + editDurationValue: null, + editDurationBlink: false }); expect(mockScreen.render).toHaveBeenCalledTimes(1); @@ -412,7 +439,10 @@ describe('DoroUi', () => { promptTotalSeconds: 0, promptNextMode: null, updatePromptState: 'none', - updateCheckResult: null + updateCheckResult: null, + editDurationState: 'none', + editDurationValue: null, + editDurationBlink: false }); expect(mockScreen.render).toHaveBeenCalledTimes(1); @@ -436,7 +466,10 @@ describe('DoroUi', () => { promptTotalSeconds: 0, promptNextMode: null, updatePromptState: 'none', - updateCheckResult: null + updateCheckResult: null, + editDurationState: 'none', + editDurationValue: null, + editDurationBlink: false }); expect(mockScreen.render).toHaveBeenCalledTimes(1); @@ -461,7 +494,10 @@ describe('DoroUi', () => { promptTotalSeconds: 0, promptNextMode: null, updatePromptState: 'none', - updateCheckResult: null + updateCheckResult: null, + editDurationState: 'none', + editDurationValue: null, + editDurationBlink: false }); expect(mockScreen.render).toHaveBeenCalledTimes(1); @@ -485,7 +521,10 @@ describe('DoroUi', () => { promptTotalSeconds: 0, promptNextMode: null, updatePromptState: 'none', - updateCheckResult: null + updateCheckResult: null, + editDurationState: 'none', + editDurationValue: null, + editDurationBlink: false }); expect(mockScreen.render).toHaveBeenCalledTimes(1); @@ -507,7 +546,10 @@ describe('DoroUi', () => { promptTotalSeconds: 0, promptNextMode: null, updatePromptState: 'available', - updateCheckResult: { isAvailable: true, currentVersion: '1.2.0' } // no latestVersion + updateCheckResult: { isAvailable: true, currentVersion: '1.2.0' }, // no latestVersion + editDurationState: 'none', + editDurationValue: null, + editDurationBlink: false }); expect(mockScreen.render).toHaveBeenCalledTimes(1); @@ -535,7 +577,10 @@ describe('DoroUi', () => { ui.render({ ...baseState, updatePromptState: 'copySuccess', - updateCheckResult: null + updateCheckResult: null, + editDurationState: 'none', + editDurationValue: null, + editDurationBlink: false }); expect(mockScreen.render).toHaveBeenCalledTimes(1); mockScreen.render.mockClear(); @@ -544,7 +589,10 @@ describe('DoroUi', () => { ui.render({ ...baseState, updatePromptState: 'copyFallback', - updateCheckResult: null + updateCheckResult: null, + editDurationState: 'none', + editDurationValue: null, + editDurationBlink: false }); expect(mockScreen.render).toHaveBeenCalledTimes(1); mockScreen.render.mockClear(); @@ -553,7 +601,10 @@ describe('DoroUi', () => { ui.render({ ...baseState, updatePromptState: 'skipped', - updateCheckResult: { isAvailable: false, currentVersion: '1.2.0' } + updateCheckResult: { isAvailable: false, currentVersion: '1.2.0' }, + editDurationState: 'none', + editDurationValue: null, + editDurationBlink: false }); expect(mockScreen.render).toHaveBeenCalledTimes(1); mockScreen.render.mockClear(); @@ -562,7 +613,10 @@ describe('DoroUi', () => { ui.render({ ...baseState, updatePromptState: 'skipped', - updateCheckResult: { isAvailable: true, latestVersion: '1.3.0', currentVersion: '1.2.0' } + updateCheckResult: { isAvailable: true, latestVersion: '1.3.0', currentVersion: '1.2.0' }, + editDurationState: 'none', + editDurationValue: null, + editDurationBlink: false }); expect(mockScreen.render).toHaveBeenCalledTimes(1); mockScreen.render.mockClear(); @@ -571,7 +625,10 @@ describe('DoroUi', () => { ui.render({ ...baseState, updatePromptState: 'error', - updateCheckResult: { isAvailable: false, currentVersion: '1.2.0', error: 'net fail' } + updateCheckResult: { isAvailable: false, currentVersion: '1.2.0', error: 'net fail' }, + editDurationState: 'none', + editDurationValue: null, + editDurationBlink: false }); expect(mockScreen.render).toHaveBeenCalledTimes(1); mockScreen.render.mockClear(); @@ -581,13 +638,149 @@ describe('DoroUi', () => { ui.render({ ...baseState, updatePromptState: 'available', - updateCheckResult: { isAvailable: true, latestVersion: '1.3.0', currentVersion: '1.2.0' } + updateCheckResult: { isAvailable: true, latestVersion: '1.3.0', currentVersion: '1.2.0' }, + editDurationState: 'none', + editDurationValue: null, + editDurationBlink: false }); expect(mockScreen.render).toHaveBeenCalledTimes(1); mockScreen.cols = 80; // Restore }); + it('should render edit duration state', () => { + ui = new DoroUi(handlers as any); + + ui.render({ + mode: 'work', + status: 'running', + remainingSeconds: 120, + durationSeconds: 1500, + isLocked: false, + volumeMode: 'normal', + hasPrompt: false, + promptCountdownSeconds: 0, + promptTotalSeconds: 0, + promptNextMode: null, + updatePromptState: 'none', + updateCheckResult: null, + editDurationState: 'editing', + editDurationValue: 25, + editDurationBlink: false + }); + + const getRenderText = () => { + // Find the banner text in the mocked setContent calls + // We look at all setContent calls across all boxed elements + const setContentCalls = vi + .mocked(blessed.box) + .mock.results.map((r) => r.value.setContent) + .filter(Boolean) + .flatMap((m) => m.mock.calls.map((c: any[]) => c[0])); + // return the latest setContent calls string (last clear/render cycle) + return setContentCalls.slice(-5).join(' '); // just checking recent ones to avoid previous renders bleeding over + }; + + expect(getRenderText()).toContain('25'); + + ui.render({ + mode: 'work', + status: 'running', + remainingSeconds: 120, + durationSeconds: 1500, + isLocked: false, + volumeMode: 'normal', + hasPrompt: false, + promptCountdownSeconds: 0, + promptTotalSeconds: 0, + promptNextMode: null, + updatePromptState: 'none', + updateCheckResult: null, + editDurationState: 'editing', + editDurationValue: 25, + editDurationBlink: true + }); + + expect(getRenderText()).not.toContain('25'); + + ui.render({ + mode: 'work', + status: 'running', + remainingSeconds: 120, + durationSeconds: 1500, + isLocked: false, + volumeMode: 'normal', + hasPrompt: false, + promptCountdownSeconds: 0, + promptTotalSeconds: 0, + promptNextMode: null, + updatePromptState: 'none', + updateCheckResult: null, + editDurationState: 'saved', + editDurationValue: null, + editDurationBlink: false + }); + + expect(getRenderText()).toContain('saved'); + }); + + it('should render mode banners with correct short/long labels on different screen widths', () => { + ui = new DoroUi(handlers as any); + const mockScreen = vi.mocked(blessed.screen).mock.results[0].value; + const getRenderText = () => { + const setContentCalls = vi + .mocked(blessed.box) + .mock.results.map((r) => r.value.setContent) + .filter(Boolean) + .flatMap((m: any) => m.mock.calls.map((c: any) => c[0])); + return setContentCalls.join(' '); + }; + + const baseState = { + status: 'running' as const, + remainingSeconds: 600, + durationSeconds: 1500, + isLocked: false, + volumeMode: 'normal' as const, + hasPrompt: false, + promptCountdownSeconds: 0, + promptTotalSeconds: 0, + promptNextMode: null, + updatePromptState: 'none' as const, + updateCheckResult: null, + editDurationState: 'none' as const, + editDurationValue: null, + editDurationBlink: false + }; + + // Test work mode + mockScreen.cols = 80; + ui.render({ ...baseState, mode: 'work' }); + expect(getRenderText()).toContain('WORK'); + + // Test short mode (wide) + mockScreen.cols = 80; + ui.render({ ...baseState, mode: 'short' }); + expect(getRenderText()).toContain('SHORT BREAK'); + + // Test short mode (narrow) + mockScreen.cols = 12; + ui.render({ ...baseState, mode: 'short' }); + expect(getRenderText()).toContain('SHORT'); + + // Test long mode (wide) + mockScreen.cols = 80; + ui.render({ ...baseState, mode: 'long' }); + expect(getRenderText()).toContain('LONG BREAK'); + + // Test long mode (narrow) + mockScreen.cols = 12; + ui.render({ ...baseState, mode: 'long' }); + expect(getRenderText()).toContain('LONG'); + + mockScreen.cols = 80; // Restore + }); + it('should return empty transition status text when terminal is too narrow', () => { ui = new DoroUi(handlers as any); const mockScreen = vi.mocked(blessed.screen).mock.results[0].value; @@ -605,7 +798,10 @@ describe('DoroUi', () => { promptTotalSeconds: 5, promptNextMode: 'short', updatePromptState: 'none', - updateCheckResult: null + updateCheckResult: null, + editDurationState: 'none', + editDurationValue: null, + editDurationBlink: false }); expect(mockScreen.render).toHaveBeenCalledTimes(1); diff --git a/src/app.ts b/src/app.ts index 1c58a74..4cc95bb 100644 --- a/src/app.ts +++ b/src/app.ts @@ -7,7 +7,18 @@ import { createWorkStartClip } from './audio/synth'; import { playClip, stopPlayback } from './audio/player'; -import { getDurationForMode } from './constants'; +import { DEFAULT_TIMER_CONFIG, type TimerConfig, getDurationForMode } from './constants'; +import { + type Settings, + saveSettings, + resetSettings, + loadSettings, + DEFAULT_WORK_MINS, + DEFAULT_SHORT_MINS, + DEFAULT_LONG_MINS, + MODE_DURATION_BOUNDS, + EDIT_SAVE_TIMEOUT_MS +} from './config'; import { isAllowedWhenLocked, isPromptConfirmEvent, @@ -17,7 +28,6 @@ import { } from './input'; import { TimerStateMachine } from './stateMachine'; import { DoroUi } from './ui'; -import { type Settings, saveSettings, resetSettings, loadSettings } from './config'; import { checkForUpdates, copyToClipboard, @@ -28,6 +38,8 @@ import { type UpdatePromptState } from './update'; +export type EditDurationState = 'none' | 'editing' | 'saved'; + export class DoroApp { private readonly machine: TimerStateMachine; @@ -51,6 +63,15 @@ export class DoroApp { private lastTickTs = Date.now(); + // Edit duration state + private editDurationState: EditDurationState = 'none'; + + private editDurationValue: number | null = null; + + private editDurationTimeout: NodeJS.Timeout | null = null; + + private editDurationBlink = false; + // Update-related state private updatePromptState: UpdatePromptState = 'none'; @@ -59,7 +80,13 @@ export class DoroApp { private isCheckingUpdate = false; public constructor(initialSettings: Settings) { - this.machine = new TimerStateMachine(); + const config: TimerConfig = { + ...DEFAULT_TIMER_CONFIG, + workSeconds: (initialSettings.workDuration ?? DEFAULT_WORK_MINS) * 60, + shortRestSeconds: (initialSettings.shortBreakDuration ?? DEFAULT_SHORT_MINS) * 60, + longRestSeconds: (initialSettings.longBreakDuration ?? DEFAULT_LONG_MINS) * 60 + }; + this.machine = new TimerStateMachine(config); this.volumeMode = initialSettings.volumeMode; const mult = this.volumeMode === 'quiet' ? 0.25 : 1.0; @@ -106,6 +133,8 @@ export class DoroApp { void this.performStartupUpdateCheck(); } + private blinkCounter = 0; + private stepClock(): void { if (this.isExiting) { return; @@ -114,6 +143,18 @@ export class DoroApp { const now = Date.now(); let state = this.machine.getState(); + // Handle blinking for edit duration (slow down blinking relative to 250ms tick) + if (this.editDurationState === 'editing') { + this.blinkCounter++; + if (this.blinkCounter >= 3) { + this.editDurationBlink = !this.editDurationBlink; + this.blinkCounter = 0; + } + } else { + this.editDurationBlink = false; + this.blinkCounter = 0; + } + if (state.status === 'running') { const elapsedSeconds = Math.floor((now - this.lastTickTs) / 1000); if (elapsedSeconds > 0) { @@ -160,6 +201,12 @@ export class DoroApp { const command = resolveControlCommand(event); + // If we're editing duration, any command other than duration commands or pause should perhaps cancel or we just let it fall through. + if (command === 'increaseDuration' || command === 'decreaseDuration') { + this.handleDurationEdit(command); + return; + } + // Handle test mode commands for VRT deterministic states if (process.env.DORO_TEST_MODE === '1') { if (command === 'testUpdateAvailable') { @@ -349,6 +396,95 @@ export class DoroApp { this.render(); } + private handleDurationEdit(command: 'increaseDuration' | 'decreaseDuration'): void { + const state = this.machine.getState(); + const config = this.machine.getConfig(); + + let justStarted = false; + if (this.editDurationState === 'none' || this.editDurationState === 'saved') { + this.editDurationState = 'editing'; + const durationSecs = getDurationForMode(config, state.mode); + this.editDurationValue = Math.floor(durationSecs / 60); + justStarted = true; + } + + if (this.editDurationValue === null) { + return; + } + + const bounds = MODE_DURATION_BOUNDS[state.mode]; + + if (!justStarted) { + if (command === 'increaseDuration') { + this.editDurationValue += 1; + } else { + this.editDurationValue -= 1; + } + } + + // Clamp to per-mode bounds (also handles out-of-range initial values) + this.editDurationValue = Math.max(bounds.min, Math.min(bounds.max, this.editDurationValue)); + + this.render(); + + if (this.editDurationTimeout) { + clearTimeout(this.editDurationTimeout); + } + + this.editDurationTimeout = setTimeout(() => { + this.saveDurationEdit(); + }, EDIT_SAVE_TIMEOUT_MS); + } + + private saveDurationEdit(): void { + if (this.editDurationState !== 'editing' || this.editDurationValue === null) { + this.clearDurationEdit(); + return; + } + + const state = this.machine.getState(); + const config = this.machine.getConfig(); + const newConfig = { ...config }; + + if (state.mode === 'short') { + newConfig.shortRestSeconds = this.editDurationValue * 60; + } else if (state.mode === 'long') { + newConfig.longRestSeconds = this.editDurationValue * 60; + } else { + newConfig.workSeconds = this.editDurationValue * 60; + } + + this.machine.updateConfig(newConfig); + + // Save to settings + void (async () => { + const currentSettings = await loadSettings(); + await saveSettings({ + ...currentSettings, + workDuration: Math.floor(newConfig.workSeconds / 60), + shortBreakDuration: Math.floor(newConfig.shortRestSeconds / 60), + longBreakDuration: Math.floor(newConfig.longRestSeconds / 60) + }); + })(); + + this.editDurationState = 'saved'; + this.render(); + + if (this.editDurationTimeout) { + clearTimeout(this.editDurationTimeout); + } + + this.editDurationTimeout = setTimeout(() => { + this.clearDurationEdit(); + }, EDIT_SAVE_TIMEOUT_MS); + } + + private clearDurationEdit(): void { + this.editDurationState = 'none'; + this.editDurationValue = null; + this.render(); + } + private playModeClip(mode: 'work' | 'short' | 'long'): void { if (this.volumeMode === 'muted') { return; @@ -409,7 +545,10 @@ export class DoroApp { promptTotalSeconds, promptNextMode, updatePromptState: this.updatePromptState, - updateCheckResult: this.updateCheckResult + updateCheckResult: this.updateCheckResult, + editDurationState: this.editDurationState, + editDurationValue: this.editDurationValue, + editDurationBlink: this.editDurationBlink }); } diff --git a/src/config.ts b/src/config.ts index 8400cd8..6a107ff 100644 --- a/src/config.ts +++ b/src/config.ts @@ -2,18 +2,37 @@ import fs from 'node:fs'; import path from 'node:path'; import envPaths from 'env-paths'; +export const DEFAULT_WORK_MINS = 22; +export const DEFAULT_SHORT_MINS = 5; +export const DEFAULT_LONG_MINS = 12; + +export const MODE_DURATION_BOUNDS: Record<'work' | 'short' | 'long', { min: number; max: number }> = + { + work: { min: 20, max: 30 }, + short: { min: 3, max: 7 }, + long: { min: 10, max: 18 } + }; + +export const EDIT_SAVE_TIMEOUT_MS = 2000; + export type Settings = { volumeMode: 'normal' | 'quiet' | 'muted'; colorScheme: 'modern' | 'calm'; lastCheckedAt?: number; checkIntervalHours?: number; skippedVersion?: string; + workDuration?: number; + shortBreakDuration?: number; + longBreakDuration?: number; }; const DEFAULT_SETTINGS: Settings = { volumeMode: 'normal', colorScheme: 'modern', - checkIntervalHours: 24 + checkIntervalHours: 24, + workDuration: DEFAULT_WORK_MINS, + shortBreakDuration: DEFAULT_SHORT_MINS, + longBreakDuration: DEFAULT_LONG_MINS }; const paths = envPaths('doro-cli'); diff --git a/src/constants.ts b/src/constants.ts index b9da35e..13c7efc 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,3 +1,5 @@ +import { DEFAULT_WORK_MINS, DEFAULT_SHORT_MINS, DEFAULT_LONG_MINS } from './config'; + export type TimerMode = 'work' | 'short' | 'long'; export type TimerStatus = 'running' | 'paused' | 'switchPrompt'; @@ -11,9 +13,9 @@ export type TimerConfig = { }; export const DEFAULT_TIMER_CONFIG: TimerConfig = { - workSeconds: 22 * 60, - shortRestSeconds: 5 * 60, - longRestSeconds: 12 * 60, + workSeconds: DEFAULT_WORK_MINS * 60, + shortRestSeconds: DEFAULT_SHORT_MINS * 60, + longRestSeconds: DEFAULT_LONG_MINS * 60, longRestEveryWorkSessions: 3, switchConfirmSeconds: 60 }; diff --git a/src/input.ts b/src/input.ts index b502ced..fa0ebd2 100644 --- a/src/input.ts +++ b/src/input.ts @@ -30,6 +30,8 @@ export type ControlCommand = | 'checkUpdate' | 'updateYes' | 'updateNo' + | 'increaseDuration' + | 'decreaseDuration' | 'testUpdateAvailable' | 'testUpdateCopySuccess' | 'testUpdateCopyFallback' @@ -117,6 +119,19 @@ export function resolveControlCommand(event: InputEvent): ControlCommand { return 'updateNo'; } + if (event.keyName === 'up' || event.keyName === 'right' || event.ch === '+' || event.ch === '=') { + return 'increaseDuration'; + } + + if ( + event.keyName === 'down' || + event.keyName === 'left' || + event.ch === '-' || + event.ch === '_' + ) { + return 'decreaseDuration'; + } + return 'none'; } diff --git a/src/stateMachine.ts b/src/stateMachine.ts index fff3c46..4cdcb71 100644 --- a/src/stateMachine.ts +++ b/src/stateMachine.ts @@ -238,6 +238,25 @@ export class TimerStateMachine { return { state: this.getState(), switchedToMode: prompt.nextMode }; } + public updateConfig(newConfig: TimerConfig): void { + // If we're modifying the duration of the current mode, we should + // also update the remaining seconds by the difference between new and old total duration. + const oldDuration = getDurationForMode(this.config, this.state.mode); + const newDuration = getDurationForMode(newConfig, this.state.mode); + const diff = newDuration - oldDuration; + + Object.assign(this.config, newConfig); + + if (diff !== 0) { + // Ensure we don't drop below 0 if the limit was shortened significantly + const newRemaining = Math.max(0, this.state.remainingSeconds + diff); + this.state = { + ...this.state, + remainingSeconds: newRemaining + }; + } + } + public getConfig(): TimerConfig { return this.config; } diff --git a/src/ui.ts b/src/ui.ts index 7ce396e..c02db5a 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -16,6 +16,9 @@ type UiRenderState = { promptNextMode: TimerMode | null; updatePromptState: UpdatePromptState; updateCheckResult: UpdateCheckResult | null; + editDurationState: 'none' | 'editing' | 'saved'; + editDurationValue: number | null; + editDurationBlink: boolean; }; type UiHandlers = { @@ -53,7 +56,7 @@ function stripBlessedTags(s: string): string { * Tags are skipped over without counting toward the position. * Returns `[left, right]` where left contains the first `n` visible characters. */ -function splitAtVisible(s: string, n: number): [string, string] { +export function splitAtVisible(s: string, n: number): [string, string] { if (n <= 0) { return ['', s]; } @@ -644,6 +647,9 @@ export class DoroUi { const palette = PALETTES[this.colorScheme]; const isTransition = state.hasPrompt; const hasUpdatePrompt = state.updatePromptState !== 'none'; + const isEditingDuration = state.editDurationState === 'editing'; + const isSavedDuration = state.editDurationState === 'saved'; + const style = isPaused ? palette.pause : isTransition && state.promptNextMode @@ -656,24 +662,29 @@ export class DoroUi { const progressWidth = Math.round(cols * progressRatio); const compactHeight = rows < 10; - const bannerText = isPaused - ? 'PAUSED' - : state.hasPrompt - ? 'Done' - : hasUpdatePrompt && cols <= 16 - ? 'UPDATE' - : state.mode === 'work' - ? 'WORK' - : state.mode === 'short' - ? cols < 14 - ? 'SHORT' - : 'SHORT BREAK' - : cols < 13 - ? 'LONG' - : 'LONG BREAK'; + let bannerText: string; + if (isEditingDuration && state.editDurationValue !== null) { + bannerText = state.editDurationBlink ? ' ' : `${state.editDurationValue}`; + } else if (isSavedDuration) { + bannerText = 'saved'; + } else if (isPaused) { + bannerText = 'PAUSED'; + } else if (state.hasPrompt) { + bannerText = 'Done'; + } else if (hasUpdatePrompt && cols <= 16) { + bannerText = 'UPDATE'; + } else if (state.mode === 'work') { + bannerText = 'WORK'; + } else if (state.mode === 'short') { + bannerText = cols < 14 ? 'SHORT' : 'SHORT BREAK'; + } else { + bannerText = cols < 13 ? 'LONG' : 'LONG BREAK'; + } let statusText: string; - if (hasUpdatePrompt) { + if (isEditingDuration || isSavedDuration) { + statusText = ''; // clear status row while editing + } else if (hasUpdatePrompt) { // Update prompts take priority over timer status statusText = getUpdatePromptText(state.updatePromptState, state.updateCheckResult, cols); } else if (state.hasPrompt && state.promptNextMode) { diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-large-done-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/calm-large-done-chromium-darwin.png index a4157cd..ad7b939 100644 Binary files a/tests/snapshots/visual.spec.mts-snapshots/calm-large-done-chromium-darwin.png and b/tests/snapshots/visual.spec.mts-snapshots/calm-large-done-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-large-edit-duration-blink-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/calm-large-edit-duration-blink-chromium-darwin.png new file mode 100644 index 0000000..d3f86b7 Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/calm-large-edit-duration-blink-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-large-edit-duration-blink-chromium-linux.png b/tests/snapshots/visual.spec.mts-snapshots/calm-large-edit-duration-blink-chromium-linux.png new file mode 100644 index 0000000..64fc640 Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/calm-large-edit-duration-blink-chromium-linux.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-large-edit-duration-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/calm-large-edit-duration-chromium-darwin.png new file mode 100644 index 0000000..dc1d078 Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/calm-large-edit-duration-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-large-edit-duration-chromium-linux.png b/tests/snapshots/visual.spec.mts-snapshots/calm-large-edit-duration-chromium-linux.png new file mode 100644 index 0000000..ceff9df Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/calm-large-edit-duration-chromium-linux.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-large-edit-duration-saved-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/calm-large-edit-duration-saved-chromium-darwin.png new file mode 100644 index 0000000..d3f86b7 Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/calm-large-edit-duration-saved-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-large-edit-duration-saved-chromium-linux.png b/tests/snapshots/visual.spec.mts-snapshots/calm-large-edit-duration-saved-chromium-linux.png new file mode 100644 index 0000000..0141d44 Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/calm-large-edit-duration-saved-chromium-linux.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-large-paused-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/calm-large-paused-chromium-darwin.png index 13533e0..7707538 100644 Binary files a/tests/snapshots/visual.spec.mts-snapshots/calm-large-paused-chromium-darwin.png and b/tests/snapshots/visual.spec.mts-snapshots/calm-large-paused-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-large-short-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/calm-large-short-chromium-darwin.png index 9421f2e..86a6f7f 100644 Binary files a/tests/snapshots/visual.spec.mts-snapshots/calm-large-short-chromium-darwin.png and b/tests/snapshots/visual.spec.mts-snapshots/calm-large-short-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-large-update-available-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/calm-large-update-available-chromium-darwin.png new file mode 100644 index 0000000..35530f3 Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/calm-large-update-available-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-large-update-copy-fallback-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/calm-large-update-copy-fallback-chromium-darwin.png new file mode 100644 index 0000000..ff9fdcb Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/calm-large-update-copy-fallback-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-large-update-copy-success-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/calm-large-update-copy-success-chromium-darwin.png new file mode 100644 index 0000000..1bfe3f5 Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/calm-large-update-copy-success-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-large-update-skipped-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/calm-large-update-skipped-chromium-darwin.png new file mode 100644 index 0000000..23bc50a Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/calm-large-update-skipped-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-large-work-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/calm-large-work-chromium-darwin.png index eacf31b..25435eb 100644 Binary files a/tests/snapshots/visual.spec.mts-snapshots/calm-large-work-chromium-darwin.png and b/tests/snapshots/visual.spec.mts-snapshots/calm-large-work-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-small-done-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/calm-small-done-chromium-darwin.png index ece1fe4..bbdf578 100644 Binary files a/tests/snapshots/visual.spec.mts-snapshots/calm-small-done-chromium-darwin.png and b/tests/snapshots/visual.spec.mts-snapshots/calm-small-done-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-small-edit-duration-blink-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/calm-small-edit-duration-blink-chromium-darwin.png new file mode 100644 index 0000000..bca14a3 Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/calm-small-edit-duration-blink-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-small-edit-duration-blink-chromium-linux.png b/tests/snapshots/visual.spec.mts-snapshots/calm-small-edit-duration-blink-chromium-linux.png new file mode 100644 index 0000000..4bc5fcb Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/calm-small-edit-duration-blink-chromium-linux.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-small-edit-duration-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/calm-small-edit-duration-chromium-darwin.png new file mode 100644 index 0000000..3d14307 Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/calm-small-edit-duration-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-small-edit-duration-chromium-linux.png b/tests/snapshots/visual.spec.mts-snapshots/calm-small-edit-duration-chromium-linux.png new file mode 100644 index 0000000..bb4095e Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/calm-small-edit-duration-chromium-linux.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-small-edit-duration-saved-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/calm-small-edit-duration-saved-chromium-darwin.png new file mode 100644 index 0000000..2dc5cc0 Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/calm-small-edit-duration-saved-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-small-edit-duration-saved-chromium-linux.png b/tests/snapshots/visual.spec.mts-snapshots/calm-small-edit-duration-saved-chromium-linux.png new file mode 100644 index 0000000..be3dbd2 Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/calm-small-edit-duration-saved-chromium-linux.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-small-paused-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/calm-small-paused-chromium-darwin.png index 16c7502..7b0b9ab 100644 Binary files a/tests/snapshots/visual.spec.mts-snapshots/calm-small-paused-chromium-darwin.png and b/tests/snapshots/visual.spec.mts-snapshots/calm-small-paused-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-small-short-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/calm-small-short-chromium-darwin.png index 0c3f9db..7f2d0c7 100644 Binary files a/tests/snapshots/visual.spec.mts-snapshots/calm-small-short-chromium-darwin.png and b/tests/snapshots/visual.spec.mts-snapshots/calm-small-short-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-small-update-available-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/calm-small-update-available-chromium-darwin.png new file mode 100644 index 0000000..f61cf58 Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/calm-small-update-available-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-small-update-copy-fallback-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/calm-small-update-copy-fallback-chromium-darwin.png new file mode 100644 index 0000000..ca9edc4 Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/calm-small-update-copy-fallback-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-small-update-copy-success-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/calm-small-update-copy-success-chromium-darwin.png new file mode 100644 index 0000000..b07608f Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/calm-small-update-copy-success-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-small-update-skipped-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/calm-small-update-skipped-chromium-darwin.png new file mode 100644 index 0000000..3ca861b Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/calm-small-update-skipped-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-small-work-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/calm-small-work-chromium-darwin.png index 62e56ab..3e9d585 100644 Binary files a/tests/snapshots/visual.spec.mts-snapshots/calm-small-work-chromium-darwin.png and b/tests/snapshots/visual.spec.mts-snapshots/calm-small-work-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-done-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-done-chromium-darwin.png index fe0c7ae..82eec9f 100644 Binary files a/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-done-chromium-darwin.png and b/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-done-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-edit-duration-blink-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-edit-duration-blink-chromium-darwin.png new file mode 100644 index 0000000..85b4f1e Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-edit-duration-blink-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-edit-duration-blink-chromium-linux.png b/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-edit-duration-blink-chromium-linux.png new file mode 100644 index 0000000..c281e8e Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-edit-duration-blink-chromium-linux.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-edit-duration-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-edit-duration-chromium-darwin.png new file mode 100644 index 0000000..85ef7af Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-edit-duration-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-edit-duration-chromium-linux.png b/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-edit-duration-chromium-linux.png new file mode 100644 index 0000000..1f5ff5b Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-edit-duration-chromium-linux.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-edit-duration-saved-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-edit-duration-saved-chromium-darwin.png new file mode 100644 index 0000000..a04dce6 Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-edit-duration-saved-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-edit-duration-saved-chromium-linux.png b/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-edit-duration-saved-chromium-linux.png new file mode 100644 index 0000000..fac010a Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-edit-duration-saved-chromium-linux.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-muted-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-muted-chromium-darwin.png new file mode 100644 index 0000000..4781669 Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-muted-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-paused-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-paused-chromium-darwin.png index 5b105f9..92f060c 100644 Binary files a/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-paused-chromium-darwin.png and b/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-paused-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-quiet-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-quiet-chromium-darwin.png new file mode 100644 index 0000000..5495ee1 Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-quiet-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-short-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-short-chromium-darwin.png index b2dbf70..e1e00f4 100644 Binary files a/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-short-chromium-darwin.png and b/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-short-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-update-available-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-update-available-chromium-darwin.png new file mode 100644 index 0000000..74eca39 Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-update-available-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-update-copy-fallback-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-update-copy-fallback-chromium-darwin.png new file mode 100644 index 0000000..5bd141f Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-update-copy-fallback-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-update-copy-success-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-update-copy-success-chromium-darwin.png new file mode 100644 index 0000000..3f53a71 Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-update-copy-success-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-update-skipped-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-update-skipped-chromium-darwin.png new file mode 100644 index 0000000..371f2a5 Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-update-skipped-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-work-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-work-chromium-darwin.png index 730a6c9..4781669 100644 Binary files a/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-work-chromium-darwin.png and b/tests/snapshots/visual.spec.mts-snapshots/calm-tiny-work-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-done-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-done-chromium-darwin.png index 6fea38e..c435549 100644 Binary files a/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-done-chromium-darwin.png and b/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-done-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-edit-duration-blink-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-edit-duration-blink-chromium-darwin.png new file mode 100644 index 0000000..2aaa145 Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-edit-duration-blink-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-edit-duration-blink-chromium-linux.png b/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-edit-duration-blink-chromium-linux.png new file mode 100644 index 0000000..466a2d9 Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-edit-duration-blink-chromium-linux.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-edit-duration-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-edit-duration-chromium-darwin.png new file mode 100644 index 0000000..e53c19d Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-edit-duration-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-edit-duration-chromium-linux.png b/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-edit-duration-chromium-linux.png new file mode 100644 index 0000000..0a262ea Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-edit-duration-chromium-linux.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-edit-duration-saved-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-edit-duration-saved-chromium-darwin.png new file mode 100644 index 0000000..fd5d87b Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-edit-duration-saved-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-edit-duration-saved-chromium-linux.png b/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-edit-duration-saved-chromium-linux.png new file mode 100644 index 0000000..1eae16a Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-edit-duration-saved-chromium-linux.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-muted-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-muted-chromium-darwin.png new file mode 100644 index 0000000..8a71b3d Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-muted-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-paused-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-paused-chromium-darwin.png index 2639c97..4e184b6 100644 Binary files a/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-paused-chromium-darwin.png and b/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-paused-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-quiet-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-quiet-chromium-darwin.png new file mode 100644 index 0000000..c7773c7 Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-quiet-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-short-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-short-chromium-darwin.png index 5525732..e9a739f 100644 Binary files a/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-short-chromium-darwin.png and b/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-short-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-update-available-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-update-available-chromium-darwin.png new file mode 100644 index 0000000..0342d68 Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-update-available-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-update-copy-fallback-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-update-copy-fallback-chromium-darwin.png new file mode 100644 index 0000000..f584216 Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-update-copy-fallback-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-update-copy-success-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-update-copy-success-chromium-darwin.png new file mode 100644 index 0000000..f182c2c Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-update-copy-success-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-update-skipped-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-update-skipped-chromium-darwin.png new file mode 100644 index 0000000..94aeb99 Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-update-skipped-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-work-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-work-chromium-darwin.png index c2846a9..8a71b3d 100644 Binary files a/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-work-chromium-darwin.png and b/tests/snapshots/visual.spec.mts-snapshots/calm-ultra-small-work-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/modern-large-done-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/modern-large-done-chromium-darwin.png index 674c572..ad7b939 100644 Binary files a/tests/snapshots/visual.spec.mts-snapshots/modern-large-done-chromium-darwin.png and b/tests/snapshots/visual.spec.mts-snapshots/modern-large-done-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/modern-large-edit-duration-blink-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/modern-large-edit-duration-blink-chromium-darwin.png new file mode 100644 index 0000000..d3f86b7 Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/modern-large-edit-duration-blink-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/modern-large-edit-duration-blink-chromium-linux.png b/tests/snapshots/visual.spec.mts-snapshots/modern-large-edit-duration-blink-chromium-linux.png new file mode 100644 index 0000000..64fc640 Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/modern-large-edit-duration-blink-chromium-linux.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/modern-large-edit-duration-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/modern-large-edit-duration-chromium-darwin.png new file mode 100644 index 0000000..dc1d078 Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/modern-large-edit-duration-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/modern-large-edit-duration-chromium-linux.png b/tests/snapshots/visual.spec.mts-snapshots/modern-large-edit-duration-chromium-linux.png new file mode 100644 index 0000000..ceff9df Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/modern-large-edit-duration-chromium-linux.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/modern-large-edit-duration-saved-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/modern-large-edit-duration-saved-chromium-darwin.png new file mode 100644 index 0000000..d3f86b7 Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/modern-large-edit-duration-saved-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/modern-large-edit-duration-saved-chromium-linux.png b/tests/snapshots/visual.spec.mts-snapshots/modern-large-edit-duration-saved-chromium-linux.png new file mode 100644 index 0000000..0141d44 Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/modern-large-edit-duration-saved-chromium-linux.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/modern-large-paused-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/modern-large-paused-chromium-darwin.png index baffcd3..7707538 100644 Binary files a/tests/snapshots/visual.spec.mts-snapshots/modern-large-paused-chromium-darwin.png and b/tests/snapshots/visual.spec.mts-snapshots/modern-large-paused-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/modern-large-short-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/modern-large-short-chromium-darwin.png index 6b2767a..86a6f7f 100644 Binary files a/tests/snapshots/visual.spec.mts-snapshots/modern-large-short-chromium-darwin.png and b/tests/snapshots/visual.spec.mts-snapshots/modern-large-short-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/modern-large-update-available-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/modern-large-update-available-chromium-darwin.png index 1bc00a1..35530f3 100644 Binary files a/tests/snapshots/visual.spec.mts-snapshots/modern-large-update-available-chromium-darwin.png and b/tests/snapshots/visual.spec.mts-snapshots/modern-large-update-available-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/modern-large-update-copy-fallback-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/modern-large-update-copy-fallback-chromium-darwin.png index 1be1441..ff9fdcb 100644 Binary files a/tests/snapshots/visual.spec.mts-snapshots/modern-large-update-copy-fallback-chromium-darwin.png and b/tests/snapshots/visual.spec.mts-snapshots/modern-large-update-copy-fallback-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/modern-large-update-copy-success-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/modern-large-update-copy-success-chromium-darwin.png index 94b5757..1bfe3f5 100644 Binary files a/tests/snapshots/visual.spec.mts-snapshots/modern-large-update-copy-success-chromium-darwin.png and b/tests/snapshots/visual.spec.mts-snapshots/modern-large-update-copy-success-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/modern-large-update-skipped-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/modern-large-update-skipped-chromium-darwin.png index 91e6c26..23bc50a 100644 Binary files a/tests/snapshots/visual.spec.mts-snapshots/modern-large-update-skipped-chromium-darwin.png and b/tests/snapshots/visual.spec.mts-snapshots/modern-large-update-skipped-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/modern-large-work-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/modern-large-work-chromium-darwin.png index dd0062b..25435eb 100644 Binary files a/tests/snapshots/visual.spec.mts-snapshots/modern-large-work-chromium-darwin.png and b/tests/snapshots/visual.spec.mts-snapshots/modern-large-work-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/modern-small-edit-duration-blink-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/modern-small-edit-duration-blink-chromium-darwin.png new file mode 100644 index 0000000..bca14a3 Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/modern-small-edit-duration-blink-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/modern-small-edit-duration-blink-chromium-linux.png b/tests/snapshots/visual.spec.mts-snapshots/modern-small-edit-duration-blink-chromium-linux.png new file mode 100644 index 0000000..4bc5fcb Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/modern-small-edit-duration-blink-chromium-linux.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/modern-small-edit-duration-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/modern-small-edit-duration-chromium-darwin.png new file mode 100644 index 0000000..3d14307 Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/modern-small-edit-duration-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/modern-small-edit-duration-chromium-linux.png b/tests/snapshots/visual.spec.mts-snapshots/modern-small-edit-duration-chromium-linux.png new file mode 100644 index 0000000..bb4095e Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/modern-small-edit-duration-chromium-linux.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/modern-small-edit-duration-saved-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/modern-small-edit-duration-saved-chromium-darwin.png new file mode 100644 index 0000000..2dc5cc0 Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/modern-small-edit-duration-saved-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/modern-small-edit-duration-saved-chromium-linux.png b/tests/snapshots/visual.spec.mts-snapshots/modern-small-edit-duration-saved-chromium-linux.png new file mode 100644 index 0000000..be3dbd2 Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/modern-small-edit-duration-saved-chromium-linux.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/modern-small-paused-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/modern-small-paused-chromium-darwin.png index 09f1298..7b0b9ab 100644 Binary files a/tests/snapshots/visual.spec.mts-snapshots/modern-small-paused-chromium-darwin.png and b/tests/snapshots/visual.spec.mts-snapshots/modern-small-paused-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/modern-small-short-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/modern-small-short-chromium-darwin.png index f310b78..7f2d0c7 100644 Binary files a/tests/snapshots/visual.spec.mts-snapshots/modern-small-short-chromium-darwin.png and b/tests/snapshots/visual.spec.mts-snapshots/modern-small-short-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/modern-small-work-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/modern-small-work-chromium-darwin.png index 75c3aff..3e9d585 100644 Binary files a/tests/snapshots/visual.spec.mts-snapshots/modern-small-work-chromium-darwin.png and b/tests/snapshots/visual.spec.mts-snapshots/modern-small-work-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/modern-tiny-edit-duration-blink-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/modern-tiny-edit-duration-blink-chromium-darwin.png new file mode 100644 index 0000000..85b4f1e Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/modern-tiny-edit-duration-blink-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/modern-tiny-edit-duration-blink-chromium-linux.png b/tests/snapshots/visual.spec.mts-snapshots/modern-tiny-edit-duration-blink-chromium-linux.png new file mode 100644 index 0000000..c281e8e Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/modern-tiny-edit-duration-blink-chromium-linux.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/modern-tiny-edit-duration-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/modern-tiny-edit-duration-chromium-darwin.png new file mode 100644 index 0000000..85ef7af Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/modern-tiny-edit-duration-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/modern-tiny-edit-duration-chromium-linux.png b/tests/snapshots/visual.spec.mts-snapshots/modern-tiny-edit-duration-chromium-linux.png new file mode 100644 index 0000000..1f5ff5b Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/modern-tiny-edit-duration-chromium-linux.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/modern-tiny-edit-duration-saved-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/modern-tiny-edit-duration-saved-chromium-darwin.png new file mode 100644 index 0000000..a04dce6 Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/modern-tiny-edit-duration-saved-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/modern-tiny-edit-duration-saved-chromium-linux.png b/tests/snapshots/visual.spec.mts-snapshots/modern-tiny-edit-duration-saved-chromium-linux.png new file mode 100644 index 0000000..fac010a Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/modern-tiny-edit-duration-saved-chromium-linux.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/modern-tiny-muted-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/modern-tiny-muted-chromium-darwin.png index 5495ee1..4781669 100644 Binary files a/tests/snapshots/visual.spec.mts-snapshots/modern-tiny-muted-chromium-darwin.png and b/tests/snapshots/visual.spec.mts-snapshots/modern-tiny-muted-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/modern-tiny-paused-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/modern-tiny-paused-chromium-darwin.png index 8f55759..92f060c 100644 Binary files a/tests/snapshots/visual.spec.mts-snapshots/modern-tiny-paused-chromium-darwin.png and b/tests/snapshots/visual.spec.mts-snapshots/modern-tiny-paused-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/modern-tiny-quiet-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/modern-tiny-quiet-chromium-darwin.png index 652316a..5495ee1 100644 Binary files a/tests/snapshots/visual.spec.mts-snapshots/modern-tiny-quiet-chromium-darwin.png and b/tests/snapshots/visual.spec.mts-snapshots/modern-tiny-quiet-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/modern-tiny-short-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/modern-tiny-short-chromium-darwin.png index f61fe96..e1e00f4 100644 Binary files a/tests/snapshots/visual.spec.mts-snapshots/modern-tiny-short-chromium-darwin.png and b/tests/snapshots/visual.spec.mts-snapshots/modern-tiny-short-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/modern-tiny-update-copy-fallback-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/modern-tiny-update-copy-fallback-chromium-darwin.png new file mode 100644 index 0000000..5bd141f Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/modern-tiny-update-copy-fallback-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/modern-tiny-update-skipped-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/modern-tiny-update-skipped-chromium-darwin.png new file mode 100644 index 0000000..371f2a5 Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/modern-tiny-update-skipped-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/modern-tiny-work-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/modern-tiny-work-chromium-darwin.png index 5495ee1..4781669 100644 Binary files a/tests/snapshots/visual.spec.mts-snapshots/modern-tiny-work-chromium-darwin.png and b/tests/snapshots/visual.spec.mts-snapshots/modern-tiny-work-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/modern-ultra-small-edit-duration-blink-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/modern-ultra-small-edit-duration-blink-chromium-darwin.png new file mode 100644 index 0000000..2aaa145 Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/modern-ultra-small-edit-duration-blink-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/modern-ultra-small-edit-duration-blink-chromium-linux.png b/tests/snapshots/visual.spec.mts-snapshots/modern-ultra-small-edit-duration-blink-chromium-linux.png new file mode 100644 index 0000000..466a2d9 Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/modern-ultra-small-edit-duration-blink-chromium-linux.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/modern-ultra-small-edit-duration-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/modern-ultra-small-edit-duration-chromium-darwin.png new file mode 100644 index 0000000..e53c19d Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/modern-ultra-small-edit-duration-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/modern-ultra-small-edit-duration-chromium-linux.png b/tests/snapshots/visual.spec.mts-snapshots/modern-ultra-small-edit-duration-chromium-linux.png new file mode 100644 index 0000000..0a262ea Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/modern-ultra-small-edit-duration-chromium-linux.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/modern-ultra-small-edit-duration-saved-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/modern-ultra-small-edit-duration-saved-chromium-darwin.png new file mode 100644 index 0000000..fd5d87b Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/modern-ultra-small-edit-duration-saved-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/modern-ultra-small-edit-duration-saved-chromium-linux.png b/tests/snapshots/visual.spec.mts-snapshots/modern-ultra-small-edit-duration-saved-chromium-linux.png new file mode 100644 index 0000000..1eae16a Binary files /dev/null and b/tests/snapshots/visual.spec.mts-snapshots/modern-ultra-small-edit-duration-saved-chromium-linux.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/modern-ultra-small-muted-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/modern-ultra-small-muted-chromium-darwin.png index c7773c7..8a71b3d 100644 Binary files a/tests/snapshots/visual.spec.mts-snapshots/modern-ultra-small-muted-chromium-darwin.png and b/tests/snapshots/visual.spec.mts-snapshots/modern-ultra-small-muted-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/modern-ultra-small-paused-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/modern-ultra-small-paused-chromium-darwin.png index 5a01fe7..4e184b6 100644 Binary files a/tests/snapshots/visual.spec.mts-snapshots/modern-ultra-small-paused-chromium-darwin.png and b/tests/snapshots/visual.spec.mts-snapshots/modern-ultra-small-paused-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/modern-ultra-small-quiet-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/modern-ultra-small-quiet-chromium-darwin.png index 2d631c0..c7773c7 100644 Binary files a/tests/snapshots/visual.spec.mts-snapshots/modern-ultra-small-quiet-chromium-darwin.png and b/tests/snapshots/visual.spec.mts-snapshots/modern-ultra-small-quiet-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/modern-ultra-small-short-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/modern-ultra-small-short-chromium-darwin.png index d826178..e9a739f 100644 Binary files a/tests/snapshots/visual.spec.mts-snapshots/modern-ultra-small-short-chromium-darwin.png and b/tests/snapshots/visual.spec.mts-snapshots/modern-ultra-small-short-chromium-darwin.png differ diff --git a/tests/snapshots/visual.spec.mts-snapshots/modern-ultra-small-work-chromium-darwin.png b/tests/snapshots/visual.spec.mts-snapshots/modern-ultra-small-work-chromium-darwin.png index c7773c7..8a71b3d 100644 Binary files a/tests/snapshots/visual.spec.mts-snapshots/modern-ultra-small-work-chromium-darwin.png and b/tests/snapshots/visual.spec.mts-snapshots/modern-ultra-small-work-chromium-darwin.png differ diff --git a/tests/visual.spec.mts b/tests/visual.spec.mts index b2979e7..e83342c 100644 --- a/tests/visual.spec.mts +++ b/tests/visual.spec.mts @@ -58,7 +58,7 @@ test.describe('Doro CLI Visual Regression', () => { ); const cliPath = path.resolve(rootDir, 'dist/cli.js'); - ptyProcess = pty.spawn('node', [cliPath], { + ptyProcess = pty.spawn(process.execPath, [cliPath], { name: 'xterm-color', cols, rows, @@ -141,6 +141,34 @@ test.describe('Doro CLI Visual Regression', () => { ); }); + // Edit duration scenarios + test('edit duration mode', async ({ page }) => { + await setupTerminal(page, size.cols, size.rows, theme); + await page.evaluate(() => (window as any).sendPtyData('\x1b[A')); // UP arrow + await page.waitForTimeout(500); // wait for edit mode + await expect(page.locator('#terminal-container')).toHaveScreenshot( + `${theme}-${size.name}-edit-duration.png` + ); + }); + + test('edit duration blink state', async ({ page }) => { + await setupTerminal(page, size.cols, size.rows, theme); + await page.evaluate(() => (window as any).sendPtyData('\x1b[A')); // UP arrow + await page.waitForTimeout(1300); // wait for blink (blink counter triggers after 3 ticks = 750ms) + await expect(page.locator('#terminal-container')).toHaveScreenshot( + `${theme}-${size.name}-edit-duration-blink.png` + ); + }); + + test('edit duration saved state', async ({ page }) => { + await setupTerminal(page, size.cols, size.rows, theme); + await page.evaluate(() => (window as any).sendPtyData('\x1b[A')); // UP arrow + await page.waitForTimeout(2500); // Wait for the 2s timeout to trigger save state + await expect(page.locator('#terminal-container')).toHaveScreenshot( + `${theme}-${size.name}-edit-duration-saved.png` + ); + }); + if (size.name === 'tiny' || size.name === 'ultra-small') { test('quiet volume mode', async ({ page }) => { await setupTerminal(page, size.cols, size.rows, theme);