Remade frontend dashboard as flutter dashboard, still WIP

This commit is contained in:
Nate Anderson
2025-06-13 09:20:42 -06:00
parent 8ea06b12f7
commit b373a93f0e
207 changed files with 9869 additions and 46 deletions
+203
View File
@@ -0,0 +1,203 @@
import 'package:test/test.dart';
import 'package:sqlite3/sqlite3.dart';
import 'package:xp_nix/src/monitors/productivity_monitor.dart';
import '../lib/src/testing/mock_idle_monitor.dart';
import '../lib/src/testing/mock_activity_detector.dart';
import '../lib/src/testing/mock_time_provider.dart';
import '../lib/src/testing/mock_desktop_enhancer.dart';
import '../lib/src/config/config_manager.dart';
void main() {
group('Deep Idle Event Tests', () {
late Database db;
late ProductivityMonitor monitor;
late MockIdleMonitor mockIdleMonitor;
late MockActivityDetector mockActivityDetector;
late MockTimeProvider mockTimeProvider;
late MockDesktopEnhancer mockDesktopEnhancer;
setUp(() async {
// Create in-memory database for testing
db = sqlite3.openInMemory();
// Reset and initialize ConfigManager with default config
ConfigManager.resetInstance();
await ConfigManager.instance.initialize('/tmp/test_config_${DateTime.now().millisecondsSinceEpoch}.json');
// Create mock dependencies
mockIdleMonitor = MockIdleMonitor();
mockActivityDetector = MockActivityDetector();
mockTimeProvider = MockTimeProvider();
mockDesktopEnhancer = MockDesktopEnhancer();
// Set up starting time (Monday 9 AM)
mockTimeProvider.setTime(DateTime(2024, 1, 1, 9, 0));
// Create testable monitor with mocked dependencies
monitor = ProductivityMonitor(
db: db,
idleMonitor: mockIdleMonitor,
activityDetector: mockActivityDetector,
timeProvider: mockTimeProvider,
desktopEnhancer: mockDesktopEnhancer,
);
});
tearDown(() async {
monitor.stop();
// Add a small delay to allow async operations to complete
await Future.delayed(Duration(milliseconds: 100));
try {
db.dispose();
} catch (e) {
// Database might already be closed, ignore the error
}
});
test('should end current activity and award XP when user goes deep idle', () async {
// Start the monitor
monitor.start();
await Future.delayed(Duration(milliseconds: 10));
print('\n🧪 Testing deep idle behavior...');
// Start a coding activity
mockActivityDetector.simulateActivity('vscode', 'main.dart - TestProject');
await Future.delayed(Duration(milliseconds: 10));
// Work for 30 minutes
mockTimeProvider.advanceTime(Duration(minutes: 30));
// Check that no activity has been saved yet (still in progress)
var stats = monitor.getTodayStats();
var initialXP = stats['xp'] as int;
print('📊 Initial XP before deep idle: $initialXP');
// User goes deep idle - this should trigger ending the current activity
print('😴 User goes deep idle...');
mockIdleMonitor.simulateDeepIdle();
await Future.delayed(Duration(milliseconds: 50)); // Allow event processing
// Check that the activity was saved and XP was awarded
stats = monitor.getTodayStats();
var finalXP = stats['xp'] as int;
var focusTime = stats['focus_time'] as int;
print('📊 Final XP after deep idle: $finalXP');
print('📊 Focus time: ${(focusTime / 60).toStringAsFixed(1)} minutes');
// Verify that XP was awarded for the 30-minute coding session
expect(finalXP, greaterThan(initialXP),
reason: 'Should have earned XP from the coding session when going deep idle');
// Expected: 30 minutes * 10 XP/min = 300 XP (base, before multipliers)
expect(finalXP, greaterThan(200),
reason: 'Should have earned substantial XP from 30 minutes of coding');
// Should have focus time from coding
expect(focusTime, greaterThan(25 * 60),
reason: 'Should have at least 25 minutes of focus time recorded');
print('✅ Deep idle activity ending test passed!');
});
test('should not duplicate XP when going from light idle to deep idle', () async {
// Start the monitor
monitor.start();
await Future.delayed(Duration(milliseconds: 10));
print('\n🧪 Testing light idle to deep idle transition...');
// Start a coding activity
mockActivityDetector.simulateActivity('vscode', 'feature.dart');
await Future.delayed(Duration(milliseconds: 10));
// Work for 20 minutes
mockTimeProvider.advanceTime(Duration(minutes: 20));
// User goes light idle first
print('😐 User goes light idle...');
mockIdleMonitor.simulateLightIdle();
await Future.delayed(Duration(milliseconds: 50));
var stats = monitor.getTodayStats();
var xpAfterLightIdle = stats['xp'] as int;
print('📊 XP after light idle: $xpAfterLightIdle');
// Then user goes deep idle
print('😴 User goes deep idle...');
mockIdleMonitor.simulateDeepIdle();
await Future.delayed(Duration(milliseconds: 50));
stats = monitor.getTodayStats();
var xpAfterDeepIdle = stats['xp'] as int;
print('📊 XP after deep idle: $xpAfterDeepIdle');
// XP should have been awarded when going deep idle, but not duplicated
expect(xpAfterDeepIdle, greaterThan(0),
reason: 'Should have earned XP from the coding session');
// The XP should be the same whether we went through light idle or not
// (since the activity should only be saved once when going deep idle)
expect(xpAfterDeepIdle, greaterThan(150),
reason: 'Should have earned XP for 20 minutes of coding');
print('✅ Light idle to deep idle transition test passed!');
});
test('should handle multiple activity sessions with deep idle interruptions', () async {
// Start the monitor
monitor.start();
await Future.delayed(Duration(milliseconds: 10));
print('\n🧪 Testing multiple sessions with deep idle interruptions...');
// First coding session
mockActivityDetector.simulateActivity('vscode', 'session1.dart');
await Future.delayed(Duration(milliseconds: 10));
mockTimeProvider.advanceTime(Duration(minutes: 25));
// Go deep idle (should end first session)
mockIdleMonitor.simulateDeepIdle();
await Future.delayed(Duration(milliseconds: 50));
var stats = monitor.getTodayStats();
var xpAfterFirstSession = stats['xp'] as int;
print('📊 XP after first session: $xpAfterFirstSession');
// User becomes active again
mockIdleMonitor.simulateActive();
await Future.delayed(Duration(milliseconds: 10));
// Second coding session
mockActivityDetector.simulateActivity('vscode', 'session2.dart');
await Future.delayed(Duration(milliseconds: 10));
mockTimeProvider.advanceTime(Duration(minutes: 35));
// Go deep idle again (should end second session)
mockIdleMonitor.simulateDeepIdle();
await Future.delayed(Duration(milliseconds: 50));
stats = monitor.getTodayStats();
var finalXP = stats['xp'] as int;
var totalFocusTime = stats['focus_time'] as int;
print('📊 Final XP after both sessions: $finalXP');
print('📊 Total focus time: ${(totalFocusTime / 60).toStringAsFixed(1)} minutes');
// Should have XP from both sessions
expect(finalXP, greaterThan(xpAfterFirstSession),
reason: 'Should have earned additional XP from second session');
// Should have substantial XP from 60 minutes total (25 + 35)
expect(finalXP, greaterThan(400),
reason: 'Should have earned substantial XP from both coding sessions');
// Should have focus time from both sessions (at least 55 minutes)
expect(totalFocusTime, greaterThan(55 * 60),
reason: 'Should have focus time from both sessions');
print('✅ Multiple sessions with deep idle test passed!');
});
});
}
@@ -0,0 +1,617 @@
import 'package:test/test.dart';
import 'package:xp_nix/src/config/hyprland_config_parser.dart';
void main() {
group('HyprlandConfigParser', () {
group('parseConfig', () {
test('should parse basic config with decoration and general sections', () {
const config = '''
# Basic Hyprland config
exec-once = waybar
\$mod = SUPER
decoration {
rounding = 10
blur {
enabled = true
passes = 2
size = 8
}
shadow {
enabled = true
range = 15
render_power = 3
}
}
general {
border_size = 2
col.active_border = rgba(7e5fddff)
col.inactive_border = rgba(595959aa)
gaps_in = 5
gaps_out = 10
}
animation=windows, 1, 7, default
animation=fade, 1, 4, default
bind = \$mod, Q, killactive
''';
final result = HyprlandConfigParser.parseConfig(config);
expect(result.baseConfig, contains('exec-once = waybar'));
expect(result.baseConfig, contains('\$mod = SUPER'));
expect(result.baseConfig, contains('bind = \$mod, Q, killactive'));
expect(result.baseConfig, isNot(contains('decoration {')));
expect(result.baseConfig, isNot(contains('general {')));
expect(result.baseConfig, isNot(contains('animation=')));
expect(result.dynamicSections, hasLength(2));
expect(result.dynamicSections['decoration'], contains('rounding = 10'));
expect(result.dynamicSections['decoration'], contains('blur {'));
expect(result.dynamicSections['general'], contains('border_size = 2'));
expect(result.animations, hasLength(2));
expect(result.animations[0], equals('animation=windows, 1, 7, default'));
expect(result.animations[1], equals('animation=fade, 1, 4, default'));
});
test('should handle nested sections within decoration', () {
const config = '''
decoration {
rounding = 15
blur {
enabled = true
passes = 3
size = 12
brightness = 1.1
contrast = 1.2
noise = 0.02
vibrancy = 0.3
vibrancy_darkness = 0.2
}
shadow {
enabled = true
range = 20
render_power = 4
color = rgba(7e5fddaa)
offset = 0 0
}
dim_inactive = true
dim_strength = 0.15
inactive_opacity = 0.85
active_opacity = 1.0
drop_shadow = true
}
''';
final result = HyprlandConfigParser.parseConfig(config);
expect(result.dynamicSections['decoration'], contains('blur {'));
expect(result.dynamicSections['decoration'], contains('enabled = true'));
expect(result.dynamicSections['decoration'], contains('passes = 3'));
expect(result.dynamicSections['decoration'], contains('vibrancy_darkness = 0.2'));
expect(result.dynamicSections['decoration'], contains('shadow {'));
expect(result.dynamicSections['decoration'], contains('color = rgba(7e5fddaa)'));
expect(result.dynamicSections['decoration'], contains('dim_inactive = true'));
});
test('should handle complex general section with gradient borders', () {
const config = '''
general {
border_size = 4
col.active_border = rgba(7e5fddff) rgba(ff5100ff) rgba(00ff88ff) 45deg
col.inactive_border = rgba(595959aa)
gaps_in = 4
gaps_out = 8
resize_on_border = true
extend_border_grab_area = 15
allow_tearing = false
layout = dwindle
}
''';
final result = HyprlandConfigParser.parseConfig(config);
expect(result.dynamicSections['general'], contains('border_size = 4'));
expect(result.dynamicSections['general'], contains('rgba(7e5fddff) rgba(ff5100ff) rgba(00ff88ff) 45deg'));
expect(result.dynamicSections['general'], contains('resize_on_border = true'));
expect(result.dynamicSections['general'], contains('extend_border_grab_area = 15'));
});
test('should preserve non-dynamic sections in base config', () {
const config = '''
input {
kb_layout = us
follow_mouse = 1
touchpad {
natural_scroll = true
}
}
misc {
disable_hyprland_logo = true
force_default_wallpaper = 0
}
decoration {
rounding = 5
}
windowrule = float, ^(pavucontrol)\$
windowrule = workspace 2, ^(firefox)\$
''';
final result = HyprlandConfigParser.parseConfig(config);
expect(result.baseConfig, contains('input {'));
expect(result.baseConfig, contains('kb_layout = us'));
expect(result.baseConfig, contains('touchpad {'));
expect(result.baseConfig, contains('misc {'));
expect(result.baseConfig, contains('windowrule = float'));
expect(result.baseConfig, isNot(contains('decoration {')));
expect(result.dynamicSections['decoration'], contains('rounding = 5'));
});
test('should handle config with comments and empty lines', () {
const config = '''
# This is a comment
exec-once = waybar
# Decoration settings
decoration {
# Rounded corners
rounding = 10
# Blur settings
blur {
enabled = true
passes = 2
}
}
# General settings
general {
border_size = 2
# Active border color
col.active_border = rgba(7e5fddff)
}
# Animation settings
animation=windows, 1, 7, default
''';
final result = HyprlandConfigParser.parseConfig(config);
expect(result.baseConfig, contains('# This is a comment'));
expect(result.baseConfig, contains('exec-once = waybar'));
expect(result.dynamicSections['decoration'], contains('# Rounded corners'));
expect(result.dynamicSections['decoration'], contains('# Blur settings'));
expect(result.dynamicSections['general'], contains('# Active border color'));
expect(result.animations[0], equals('animation=windows, 1, 7, default'));
});
test('should handle config with no dynamic sections', () {
const config = '''
exec-once = waybar
\$mod = SUPER
input {
kb_layout = us
}
bind = \$mod, Q, killactive
animation=windows, 1, 7, default
''';
final result = HyprlandConfigParser.parseConfig(config);
expect(result.baseConfig, contains('exec-once = waybar'));
expect(result.baseConfig, contains('input {'));
expect(result.baseConfig, contains('bind = \$mod, Q, killactive'));
expect(result.dynamicSections, isEmpty);
expect(result.animations, hasLength(1));
});
test('should handle inline animations in base config', () {
const config = '''
exec-once = waybar
animation=windows, 1, 7, default
animation=fade, 1, 4, default
decoration {
rounding = 10
}
animation=workspaces, 1, 6, default
''';
final result = HyprlandConfigParser.parseConfig(config);
expect(result.animations, hasLength(3));
expect(result.animations, contains('animation=windows, 1, 7, default'));
expect(result.animations, contains('animation=fade, 1, 4, default'));
expect(result.animations, contains('animation=workspaces, 1, 6, default'));
expect(result.baseConfig, isNot(contains('animation=')));
});
});
group('validateConfig', () {
test('should validate complete config as valid', () {
const config = '''
decoration {
rounding = 10
blur {
enabled = true
passes = 2
size = 8
}
shadow {
enabled = true
range = 15
render_power = 3
}
}
general {
border_size = 2
col.active_border = rgba(7e5fddff)
col.inactive_border = rgba(595959aa)
gaps_in = 5
gaps_out = 10
}
animation=windows, 1, 7, default
''';
final result = HyprlandConfigParser.validateConfig(config);
expect(result.isValid, isTrue);
expect(result.issues, isEmpty);
expect(result.foundSections, containsAll(['decoration', 'general']));
expect(result.hasAnimations, isTrue);
});
test('should identify missing decoration properties', () {
const config = '''
decoration {
rounding = 10
# Missing blur and shadow sections
}
general {
border_size = 2
col.active_border = rgba(7e5fddff)
col.inactive_border = rgba(595959aa)
gaps_in = 5
gaps_out = 10
}
''';
final result = HyprlandConfigParser.validateConfig(config);
expect(result.isValid, isFalse);
expect(result.issues, contains('Missing decoration property: blur'));
expect(result.issues, contains('Missing decoration property: shadow'));
expect(result.issues, contains('No animation definitions found'));
});
test('should identify missing blur sub-properties', () {
const config = '''
decoration {
rounding = 10
blur {
enabled = true
# Missing passes and size
}
shadow {
enabled = true
range = 15
render_power = 3
}
}
''';
final result = HyprlandConfigParser.validateConfig(config);
expect(result.isValid, isFalse);
expect(result.issues, contains('Missing blur property: passes'));
expect(result.issues, contains('Missing blur property: size'));
});
test('should identify missing general properties', () {
const config = '''
general {
border_size = 2
# Missing color and gap properties
}
''';
final result = HyprlandConfigParser.validateConfig(config);
expect(result.isValid, isFalse);
expect(result.issues, contains('Missing general property: col.active_border'));
expect(result.issues, contains('Missing general property: col.inactive_border'));
expect(result.issues, contains('Missing general property: gaps_in'));
expect(result.issues, contains('Missing general property: gaps_out'));
});
test('should accept inline animations in base config', () {
const config = '''
exec-once = waybar
animation=windows, 1, 7, default
decoration {
rounding = 10
blur {
enabled = true
passes = 2
size = 8
}
shadow {
enabled = true
range = 15
render_power = 3
}
}
general {
border_size = 2
col.active_border = rgba(7e5fddff)
col.inactive_border = rgba(595959aa)
gaps_in = 5
gaps_out = 10
}
''';
final result = HyprlandConfigParser.validateConfig(config);
expect(result.isValid, isTrue);
expect(result.hasAnimations, isTrue);
});
});
group('extractStylingProperties', () {
test('should extract all styling properties from complete config', () {
const config = '''
decoration {
rounding = 15
blur {
enabled = true
passes = 3
size = 12
brightness = 1.1
}
shadow {
enabled = true
range = 20
render_power = 4
}
dim_inactive = true
active_opacity = 1.0
}
general {
border_size = 4
col.active_border = rgba(7e5fddff) rgba(ff5100ff) 45deg
col.inactive_border = rgba(595959aa)
gaps_in = 4
gaps_out = 8
resize_on_border = true
}
animation=windows, 1, 8, easeout, slide
animation=fade, 1, 7, easeout
''';
final styling = HyprlandConfigParser.extractStylingProperties(config);
expect(styling['decoration'], isA<Map<String, dynamic>>());
expect(styling['general'], isA<Map<String, dynamic>>());
expect(styling['animations'], isA<List<String>>());
final decoration = styling['decoration'] as Map<String, dynamic>;
expect(decoration['rounding'], equals('15'));
expect(decoration['dim_inactive'], equals('true'));
expect(decoration['active_opacity'], equals('1.0'));
final general = styling['general'] as Map<String, dynamic>;
expect(general['border_size'], equals('4'));
expect(general['col.active_border'], equals('rgba(7e5fddff) rgba(ff5100ff) 45deg'));
expect(general['resize_on_border'], equals('true'));
final animations = styling['animations'] as List<String>;
expect(animations, hasLength(2));
expect(animations, contains('animation=windows, 1, 8, easeout, slide'));
expect(animations, contains('animation=fade, 1, 7, easeout'));
});
test('should handle nested properties correctly', () {
const config = '''
decoration {
blur {
enabled = true
passes = 2
size = 8
brightness = 1.05
contrast = 1.1
noise = 0.01
vibrancy = 0.2
}
shadow {
enabled = true
range = 15
render_power = 3
color = rgba(7e5fdd88)
offset = 0 0
}
}
''';
final styling = HyprlandConfigParser.extractStylingProperties(config);
final decoration = styling['decoration'] as Map<String, dynamic>;
// Note: The current implementation extracts nested properties as flat key-value pairs
// This is a limitation but matches the current parsing behavior
expect(decoration['enabled'], equals('true'));
expect(decoration['passes'], equals('2'));
expect(decoration['brightness'], equals('1.05'));
expect(decoration['vibrancy'], equals('0.2'));
expect(decoration['color'], equals('rgba(7e5fdd88)'));
expect(decoration['offset'], equals('0 0'));
});
test('should extract inline animations from base config', () {
const config = '''
exec-once = waybar
animation=windows, 1, 7, default
bind = \$mod, Q, killactive
animation=fade, 1, 4, default
decoration {
rounding = 10
}
''';
final styling = HyprlandConfigParser.extractStylingProperties(config);
expect(styling['animations'], isA<List<String>>());
final animations = styling['animations'] as List<String>;
expect(animations, hasLength(2));
expect(animations, contains('animation=windows, 1, 7, default'));
expect(animations, contains('animation=fade, 1, 4, default'));
});
test('should handle config with only some styling sections', () {
const config = '''
decoration {
rounding = 8
blur {
enabled = false
}
}
# No general section
animation=workspaces, 1, 6, default
''';
final styling = HyprlandConfigParser.extractStylingProperties(config);
expect(styling['decoration'], isA<Map<String, dynamic>>());
expect(styling['general'], isNull);
expect(styling['animations'], isA<List<String>>());
final decoration = styling['decoration'] as Map<String, dynamic>;
expect(decoration['rounding'], equals('8'));
expect(decoration['enabled'], equals('false'));
});
});
group('buildFullConfig', () {
test('should reconstruct config with dynamic sections', () {
const baseConfig = '''
exec-once = waybar
\$mod = SUPER
bind = \$mod, Q, killactive
''';
const decorationConfig = '''
decoration {
rounding = 10
blur {
enabled = true
}
}
''';
const generalConfig = '''
general {
border_size = 2
gaps_in = 5
}
''';
const animationConfig = '''
animation=windows, 1, 7, default
animation=fade, 1, 4, default
''';
final config = HyprlandConfig(baseConfig: baseConfig, dynamicSections: {}, animations: []);
final fullConfig = config.buildFullConfig(
decorationConfig: decorationConfig,
generalConfig: generalConfig,
animationConfig: animationConfig,
);
expect(fullConfig, contains('exec-once = waybar'));
expect(fullConfig, contains('\$mod = SUPER'));
expect(fullConfig, contains('decoration {'));
expect(fullConfig, contains('rounding = 10'));
expect(fullConfig, contains('general {'));
expect(fullConfig, contains('border_size = 2'));
expect(fullConfig, contains('animation=windows, 1, 7, default'));
expect(fullConfig, contains('animation=fade, 1, 4, default'));
});
});
group('edge cases', () {
test('should handle empty config', () {
const config = '';
final result = HyprlandConfigParser.parseConfig(config);
expect(result.baseConfig, isEmpty);
expect(result.dynamicSections, isEmpty);
expect(result.animations, isEmpty);
});
test('should handle config with only comments', () {
const config = '''
# This is a comment
# Another comment
''';
final result = HyprlandConfigParser.parseConfig(config);
expect(result.baseConfig, contains('# This is a comment'));
expect(result.dynamicSections, isEmpty);
expect(result.animations, isEmpty);
});
test('should handle malformed sections gracefully', () {
const config = '''
decoration {
rounding = 10
# Missing closing brace
general {
border_size = 2
}
''';
final result = HyprlandConfigParser.parseConfig(config);
// Should still parse what it can
expect(result.dynamicSections['general'], contains('border_size = 2'));
});
test('should handle properties with equals signs in values', () {
const config = '''
general {
col.active_border = rgba(7e5fddff) rgba(ff5100ff) rgba(00ff88ff) 45deg
custom_prop = value=with=equals
}
''';
final styling = HyprlandConfigParser.extractStylingProperties(config);
final general = styling['general'] as Map<String, dynamic>;
expect(general['col.active_border'], equals('rgba(7e5fddff) rgba(ff5100ff) rgba(00ff88ff) 45deg'));
expect(general['custom_prop'], equals('value=with=equals'));
});
});
});
}
@@ -0,0 +1,120 @@
import 'package:test/test.dart';
import 'package:sqlite3/sqlite3.dart';
import 'package:xp_nix/src/config/config_manager.dart';
import 'package:xp_nix/src/monitors/productivity_monitor.dart';
import 'package:xp_nix/src/testing/mock_activity_detector.dart';
import 'package:xp_nix/src/testing/mock_desktop_enhancer.dart';
import 'package:xp_nix/src/testing/mock_idle_monitor.dart';
import 'package:xp_nix/src/testing/mock_time_provider.dart';
void main() {
group('Consolidation Tests', () {
late Database db;
late ProductivityMonitor monitor;
late MockIdleMonitor mockIdleMonitor;
late MockActivityDetector mockActivityDetector;
late MockTimeProvider mockTimeProvider;
late MockDesktopEnhancer mockDesktopEnhancer;
setUp(() async {
// Create in-memory database for testing
db = sqlite3.openInMemory();
// Reset and initialize ConfigManager with default config
ConfigManager.resetInstance();
await ConfigManager.instance.initialize('/tmp/test_config_${DateTime.now().millisecondsSinceEpoch}.json');
// Create mock dependencies
mockIdleMonitor = MockIdleMonitor();
mockActivityDetector = MockActivityDetector();
mockTimeProvider = MockTimeProvider();
mockDesktopEnhancer = MockDesktopEnhancer();
// Set up starting time
mockTimeProvider.setTime(DateTime(2024, 1, 1, 9, 0));
// Create monitor with mocked dependencies
monitor = ProductivityMonitor(
db: db,
idleMonitor: mockIdleMonitor,
timeProvider: mockTimeProvider,
desktopEnhancer: mockDesktopEnhancer,
activityDetector: mockActivityDetector,
);
});
tearDown(() async {
monitor.stop();
// Add a small delay to allow async operations to complete
await Future.delayed(Duration(milliseconds: 100));
try {
db.dispose();
} catch (e) {
// Database might already be closed, ignore the error
}
});
test('unified monitor can be created and started', () async {
expect(() => monitor.start(), returnsNormally);
expect(monitor.getTodayStats(), isA<Map<String, dynamic>>());
});
test('unified monitor processes activities correctly', () async {
monitor.start();
await Future.delayed(Duration(milliseconds: 10));
// Simulate coding activity
mockActivityDetector.simulateActivity('vscode', 'test.dart');
await Future.delayed(Duration(milliseconds: 10)); // Allow event processing
mockTimeProvider.advanceTime(Duration(minutes: 30));
monitor.flushCurrentActivityForced();
final stats = monitor.getTodayStats();
expect(stats['xp'], greaterThan(0));
expect(stats['focus_time'], greaterThan(0));
});
test('unified monitor handles level ups', () async {
monitor.start();
await Future.delayed(Duration(milliseconds: 10));
// Simulate enough activity to level up
mockActivityDetector.simulateActivity('vscode', 'big_project.dart');
await Future.delayed(Duration(milliseconds: 10)); // Allow event processing
mockTimeProvider.advanceTime(Duration(hours: 1));
monitor.flushCurrentActivityForced();
// Manually trigger level up check
await monitor.checkForLevelUpNow();
final stats = monitor.getTodayStats();
expect(stats['level'], greaterThanOrEqualTo(2));
// Verify desktop enhancer was called
print('Desktop enhancer operations: ${mockDesktopEnhancer.operations}');
expect(mockDesktopEnhancer.operations, isNotEmpty);
expect(mockDesktopEnhancer.operations.any((op) => op.contains('celebrateLevelUp')), isTrue);
});
test('unified monitor handles idle detection', () async {
monitor.start();
await Future.delayed(Duration(milliseconds: 10));
// Start active
mockIdleMonitor.simulateActive();
mockActivityDetector.simulateActivity('vscode', 'work.dart');
mockTimeProvider.advanceTime(Duration(minutes: 30));
monitor.flushCurrentActivity();
// Go idle
mockIdleMonitor.simulateIdle();
mockTimeProvider.advanceTime(Duration(minutes: 90));
// Come back active
mockIdleMonitor.simulateActive();
final stats = monitor.getTodayStats();
expect(stats['focus_sessions'], greaterThan(0));
});
});
}
@@ -0,0 +1,187 @@
import 'package:test/test.dart';
import 'package:sqlite3/sqlite3.dart';
import 'package:xp_nix/src/monitors/productivity_monitor.dart';
import '../../lib/src/testing/mock_idle_monitor.dart';
import '../../lib/src/testing/mock_activity_detector.dart';
import '../../lib/src/testing/mock_time_provider.dart';
import '../../lib/src/testing/mock_desktop_enhancer.dart';
import '../../lib/src/config/config_manager.dart';
void main() {
group('Simple Productivity Simulation Tests', () {
late Database db;
late ProductivityMonitor monitor;
late MockIdleMonitor mockIdleMonitor;
late MockActivityDetector mockActivityDetector;
late MockTimeProvider mockTimeProvider;
late MockDesktopEnhancer mockDesktopEnhancer;
setUp(() async {
// Create in-memory database for testing
db = sqlite3.openInMemory();
// Reset and initialize ConfigManager with default config
ConfigManager.resetInstance();
await ConfigManager.instance.initialize('/tmp/test_config_${DateTime.now().millisecondsSinceEpoch}.json');
// Create mock dependencies
mockIdleMonitor = MockIdleMonitor();
mockActivityDetector = MockActivityDetector();
mockTimeProvider = MockTimeProvider();
mockDesktopEnhancer = MockDesktopEnhancer();
// Set up starting time (Monday 9 AM)
mockTimeProvider.setTime(DateTime(2024, 1, 1, 9, 0));
// Create testable monitor with mocked dependencies
monitor = ProductivityMonitor(
db: db,
idleMonitor: mockIdleMonitor,
activityDetector: mockActivityDetector,
timeProvider: mockTimeProvider,
desktopEnhancer: mockDesktopEnhancer,
);
});
tearDown(() async {
monitor.stop();
// Add a small delay to allow async operations to complete
await Future.delayed(Duration(milliseconds: 100));
try {
db.dispose();
} catch (e) {
// Database might already be closed, ignore the error
}
});
test('simulates basic coding session with XP calculation', () async {
// Start the monitor
monitor.start();
await Future.delayed(Duration(milliseconds: 10));
print('\n💻 Starting coding session...');
// Simulate 1 hour of coding
mockActivityDetector.simulateActivity('vscode', 'main.dart - TestProject');
await Future.delayed(Duration(milliseconds: 1)); // Allow event to be processed
mockTimeProvider.advanceTime(Duration(minutes: 60));
monitor.flushCurrentActivity();
// Check stats
final stats = monitor.getTodayStats();
print(
'📊 Stats: ${stats['xp']} XP, Level ${stats['level']}, ${(stats['focus_time'] / 3600).toStringAsFixed(1)}h focus',
);
// Verify XP was earned
expect(stats['xp'], greaterThan(0), reason: 'Should have earned XP from coding');
expect(stats['focus_time'], greaterThan(0), reason: 'Should have focus time from coding');
expect(stats['level'], greaterThanOrEqualTo(1), reason: 'Should have at least level 1');
// Expected: 60 minutes * 10 XP/min = 600 XP (plus any time multipliers)
expect(stats['xp'], greaterThan(500), reason: 'Should have substantial XP from 1 hour coding');
print('✅ Basic coding session test passed!');
});
test('simulates mixed activity day', () async {
monitor.start();
await Future.delayed(Duration(milliseconds: 10));
print('\n🔄 Simulating mixed activity day...');
// Morning coding (2 hours)
mockActivityDetector.simulateActivity('vscode', 'feature.dart');
await Future.delayed(Duration(milliseconds: 1));
mockTimeProvider.advanceTime(Duration(minutes: 120));
monitor.flushCurrentActivity();
// Meeting (1 hour)
mockActivityDetector.simulateActivity('zoom', 'Team Meeting');
await Future.delayed(Duration(milliseconds: 1));
mockTimeProvider.advanceTime(Duration(minutes: 60));
monitor.flushCurrentActivity();
// Research (30 minutes)
mockActivityDetector.simulateActivity('firefox', 'Documentation - dart.dev');
await Future.delayed(Duration(milliseconds: 1));
mockTimeProvider.advanceTime(Duration(minutes: 30));
monitor.flushCurrentActivity();
final stats = monitor.getTodayStats();
print('📊 Mixed day stats: ${stats['xp']} XP, Level ${stats['level']}');
print(
' Focus: ${(stats['focus_time'] / 3600).toStringAsFixed(1)}h, Meetings: ${(stats['meeting_time'] / 3600).toStringAsFixed(1)}h',
);
// Verify different activity types
expect(stats['xp'], greaterThan(1000), reason: 'Should have substantial XP from mixed activities');
expect(stats['focus_time'], greaterThan(0), reason: 'Should have focus time from coding and research');
expect(stats['meeting_time'], greaterThan(0), reason: 'Should have meeting time from zoom');
print('✅ Mixed activity day test passed!');
});
test('demonstrates XP calculation accuracy', () async {
monitor.start();
await Future.delayed(Duration(milliseconds: 10));
print('\n🧮 Testing XP calculation accuracy...');
// Test different activity types with known durations
final activities = [
('vscode', 'coding', 30, 10), // 30 min coding at 10 XP/min = 300 XP
('firefox', 'research', 20, 6), // 20 min research at 6 XP/min = 120 XP
('slack', 'collaboration', 15, 7), // 15 min collaboration at 7 XP/min = 105 XP
('zoom', 'meeting', 60, 3), // 60 min meeting at 3 XP/min = 180 XP
];
int expectedTotalXP = 0;
for (final (app, description, minutes, xpPerMin) in activities) {
mockActivityDetector.simulateActivity(app, description);
await Future.delayed(Duration(milliseconds: 1));
mockTimeProvider.advanceTime(Duration(minutes: minutes));
monitor.flushCurrentActivity();
expectedTotalXP += minutes * xpPerMin;
print(' $description: $minutes min × $xpPerMin XP/min = ${minutes * xpPerMin} XP');
}
final stats = monitor.getTodayStats();
final actualXP = stats['xp'] as int;
print('📊 Expected: ~$expectedTotalXP XP, Actual: $actualXP XP');
// Allow for time multipliers (should be close to expected)
expect(actualXP, greaterThan(expectedTotalXP * 0.8), reason: 'XP should be reasonably close to expected');
expect(actualXP, lessThan(expectedTotalXP * 2.0), reason: 'XP should not be wildly inflated');
print('✅ XP calculation accuracy test passed!');
});
test('verifies level progression', () async {
monitor.start();
await Future.delayed(Duration(milliseconds: 10));
print('\n📈 Testing level progression...');
// Simulate enough activity to level up (need 100+ XP per level)
for (int i = 0; i < 5; i++) {
mockActivityDetector.simulateActivity('vscode', 'level_up_test_$i.dart');
await Future.delayed(Duration(milliseconds: 1));
mockTimeProvider.advanceTime(Duration(minutes: 30)); // 30 min * 10 XP/min = 300 XP
monitor.flushCurrentActivity();
final stats = monitor.getTodayStats();
print(' Session ${i + 1}: ${stats['xp']} XP, Level ${stats['level']}');
}
final finalStats = monitor.getTodayStats();
expect(finalStats['level'], greaterThan(1), reason: 'Should have leveled up');
expect(finalStats['xp'], greaterThan(1000), reason: 'Should have substantial XP');
print('✅ Level progression test passed!');
});
});
}
@@ -0,0 +1,267 @@
import 'package:test/test.dart';
import 'package:sqlite3/sqlite3.dart';
import 'package:xp_nix/src/monitors/productivity_monitor.dart';
import 'package:xp_nix/src/testing/mock_idle_monitor.dart';
import 'package:xp_nix/src/testing/mock_activity_detector.dart';
import 'package:xp_nix/src/testing/mock_time_provider.dart';
import 'package:xp_nix/src/testing/mock_desktop_enhancer.dart';
import 'package:xp_nix/src/config/config_manager.dart';
void main() {
group('Work Day Simulation Tests', () {
late Database db;
late ProductivityMonitor monitor;
late MockIdleMonitor mockIdleMonitor;
late MockActivityDetector mockActivityDetector;
late MockTimeProvider mockTimeProvider;
late MockDesktopEnhancer mockDesktopEnhancer;
setUp(() async {
// Create in-memory database for testing
db = sqlite3.openInMemory();
// Reset and initialize ConfigManager with default config
ConfigManager.resetInstance();
await ConfigManager.instance.initialize('/tmp/test_config_${DateTime.now().millisecondsSinceEpoch}.json');
// Create mock dependencies
mockIdleMonitor = MockIdleMonitor();
mockActivityDetector = MockActivityDetector();
mockTimeProvider = MockTimeProvider();
mockDesktopEnhancer = MockDesktopEnhancer();
// Set up starting time (Monday 9 AM)
mockTimeProvider.setTime(DateTime(2024, 1, 1, 9, 0));
// Create monitor with mocked dependencies
monitor = ProductivityMonitor(
db: db,
idleMonitor: mockIdleMonitor,
timeProvider: mockTimeProvider,
desktopEnhancer: mockDesktopEnhancer,
activityDetector: mockActivityDetector,
);
});
tearDown(() async {
monitor.stop();
// Add a small delay to allow async operations to complete
await Future.delayed(Duration(milliseconds: 100));
try {
db.dispose();
} catch (e) {
// Database might already be closed, ignore the error
}
});
test('simulates a full productive work day', () async {
// Start the monitor
monitor.start();
// Wait a moment for initialization
await Future.delayed(Duration(milliseconds: 10));
// === MORNING CODING SESSION (9:00 - 10:30) ===
print('\n🌅 Starting morning coding session...');
mockActivityDetector.simulateActivity('vscode', 'main.dart - ProductivityApp');
mockTimeProvider.advanceTime(Duration(minutes: 90));
monitor.flushCurrentActivity(); // Save the activity
// Simulate switching to a different file
mockActivityDetector.simulateActivity('vscode', 'database_manager.dart - ProductivityApp');
mockTimeProvider.advanceTime(Duration(minutes: 30));
monitor.flushCurrentActivity(); // Save the activity
// Check stats after morning coding
var stats = monitor.getTodayStats();
expect(stats['xp'], greaterThan(0), reason: 'Should have earned XP from coding');
expect(stats['focus_time'], greaterThan(0), reason: 'Should have focus time from coding');
print('📊 After morning coding: ${stats['xp']} XP, ${(stats['focus_time'] / 3600).toStringAsFixed(1)}h focus');
// === BRIEF BREAK - CHECK SLACK (10:30 - 10:45) ===
print('\n💬 Quick Slack check...');
mockActivityDetector.simulateActivity('slack', 'General Channel');
mockTimeProvider.advanceTime(Duration(minutes: 15));
monitor.flushCurrentActivity();
// === RESEARCH SESSION (10:45 - 11:30) ===
print('\n🔍 Research session...');
mockActivityDetector.simulateActivity('firefox', 'Dart Documentation - dart.dev');
mockTimeProvider.advanceTime(Duration(minutes: 45));
monitor.flushCurrentActivity();
// === TEAM MEETING (11:30 - 12:30) ===
print('\n📅 Team meeting...');
mockActivityDetector.simulateActivity('zoom', 'Weekly Team Standup');
mockTimeProvider.advanceTime(Duration(minutes: 60));
monitor.flushCurrentActivity();
// Check stats after meeting
stats = monitor.getTodayStats();
expect(stats['meeting_time'], greaterThan(0), reason: 'Should have meeting time');
print('📊 After meeting: ${stats['xp']} XP, ${(stats['meeting_time'] / 3600).toStringAsFixed(1)}h meetings');
// === LUNCH BREAK - GO IDLE (12:30 - 13:30) ===
print('\n🍽️ Lunch break - going idle...');
mockIdleMonitor.simulateIdle();
mockTimeProvider.advanceTime(Duration(minutes: 60));
// Come back from lunch
print('\n🔄 Back from lunch...');
mockIdleMonitor.simulateActive();
// Should get focus session bonus for the morning work
stats = monitor.getTodayStats();
expect(stats['focus_sessions'], greaterThan(0), reason: 'Should have focus sessions from idle recovery');
print('📊 After lunch: ${stats['focus_sessions']} focus sessions completed');
// === AFTERNOON CODING SPRINT (13:30 - 15:30) ===
print('\n⚡ Afternoon coding sprint...');
mockActivityDetector.simulateActivity('vscode', 'test_suite.dart - ProductivityApp');
mockTimeProvider.advanceTime(Duration(minutes: 120));
monitor.flushCurrentActivity();
// === DOCUMENTATION WORK (15:30 - 16:30) ===
print('\n📝 Documentation work...');
mockActivityDetector.simulateActivity('vscode', 'README.md - ProductivityApp');
mockTimeProvider.advanceTime(Duration(minutes: 60));
monitor.flushCurrentActivity();
// === END OF DAY WRAP-UP (16:30 - 17:00) ===
print('\n📋 End of day wrap-up...');
mockActivityDetector.simulateActivity('slack', 'Daily Summary');
mockTimeProvider.advanceTime(Duration(minutes: 30));
monitor.flushCurrentActivity();
// === FINAL STATS VERIFICATION ===
print('\n📈 Final day statistics:');
stats = monitor.getTodayStats();
monitor.printDetailedStats();
// Verify XP calculations
expect(stats['xp'], greaterThan(1000), reason: 'Should have substantial XP from full work day');
expect(stats['level'], greaterThanOrEqualTo(2), reason: 'Should have leveled up');
// Verify focus time (coding + research)
final focusHours = stats['focus_time'] / 3600;
expect(focusHours, greaterThan(4), reason: 'Should have 4+ hours of focus time');
expect(focusHours, lessThan(6), reason: 'Focus time should be reasonable');
// Verify meeting time
final meetingHours = stats['meeting_time'] / 3600;
expect(meetingHours, greaterThan(0.9), reason: 'Should have ~1 hour of meeting time');
expect(meetingHours, lessThan(1.1), reason: 'Meeting time should be accurate');
// Verify focus sessions
expect(stats['focus_sessions'], greaterThanOrEqualTo(1), reason: 'Should have at least 1 focus session');
// Test specific XP calculations
_verifyXPCalculations(stats);
print('\n✅ Work day simulation completed successfully!');
print(
'📊 Final Stats: Level ${stats['level']}, ${stats['xp']} XP, ${focusHours.toStringAsFixed(1)}h focus, ${stats['focus_sessions']} sessions',
);
});
test('simulates rapid context switching day', () async {
monitor.start();
await Future.delayed(Duration(milliseconds: 10));
print('\n🔄 Simulating rapid context switching...');
// Simulate a day with lots of short activities
final activities = [
('vscode', 'main.dart', 15), // 15 min coding
('slack', 'General', 5), // 5 min slack
('firefox', 'Stack Overflow', 10), // 10 min research
('vscode', 'test.dart', 20), // 20 min coding
('zoom', 'Quick sync', 15), // 15 min meeting
('vscode', 'bug_fix.dart', 25), // 25 min coding
('slack', 'Code review', 10), // 10 min collaboration
];
for (final (app, title, minutes) in activities) {
mockActivityDetector.simulateActivity(app, title);
mockTimeProvider.advanceTime(Duration(minutes: minutes));
monitor.flushCurrentActivity(); // Flush each activity
}
final stats = monitor.getTodayStats();
// Should still accumulate reasonable XP despite context switching
expect(stats['xp'], greaterThan(200), reason: 'Should earn XP from varied activities');
expect(stats['focus_time'], greaterThan(0), reason: 'Should have some focus time');
print('📊 Context switching day: ${stats['xp']} XP, ${(stats['focus_time'] / 3600).toStringAsFixed(1)}h focus');
});
test('simulates achievement unlocking', () async {
monitor.start();
await Future.delayed(Duration(milliseconds: 10));
print('\n🏆 Testing achievement unlocking...');
// Simulate extended coding to trigger achievements
mockActivityDetector.simulateActivity('vscode', 'epic_feature.dart');
// Advance time to accumulate enough XP for level 5 (need ~500 XP)
// 30 min * 10 XP/min = 300 XP per flush, need at least 2 flushes
for (int i = 0; i < 3; i++) {
mockTimeProvider.advanceTime(Duration(minutes: 60)); // 1 hour each
monitor.flushCurrentActivity();
await Future.delayed(Duration(milliseconds: 1)); // Allow level check
}
final stats = monitor.getTodayStats();
expect(stats['level'], greaterThanOrEqualTo(2), reason: 'Should reach at least level 2');
expect(stats['xp'], greaterThan(500), reason: 'Should have substantial XP');
print('📊 Achievement test: Level ${stats['level']}, ${stats['xp']} XP');
});
test('verifies idle time handling', () async {
monitor.start();
await Future.delayed(Duration(milliseconds: 10));
print('\n😴 Testing idle time handling...');
// Start with some activity and make sure user is active
mockIdleMonitor.simulateActive(); // Ensure we start active
mockActivityDetector.simulateActivity('vscode', 'work.dart');
mockTimeProvider.advanceTime(Duration(minutes: 30));
monitor.flushCurrentActivity();
// Go idle for extended period
mockIdleMonitor.simulateIdle();
mockTimeProvider.advanceTime(Duration(minutes: 90)); // 1.5 hours idle
// Come back active - this should trigger focus session calculation
mockIdleMonitor.simulateActive();
// Should get focus session bonus
final stats = monitor.getTodayStats();
expect(stats['focus_sessions'], greaterThan(0), reason: 'Should award focus session after idle period');
print('📊 Idle handling: ${stats['focus_sessions']} focus sessions awarded');
});
});
}
/// Verify that XP calculations are working correctly
void _verifyXPCalculations(Map<String, dynamic> stats) {
print('\n🧮 Verifying XP calculations...');
// Expected XP breakdown (approximate):
// - Coding: ~4 hours * 60 min * 10 XP/min = ~2400 XP
// - Research: ~45 min * 6 XP/min = ~270 XP
// - Meetings: ~60 min * 3 XP/min = ~180 XP
// - Collaboration: ~45 min * 7 XP/min = ~315 XP
// - Focus session bonuses: varies
final totalXP = stats['xp'] as int;
expect(totalXP, greaterThan(2000), reason: 'Should have substantial XP from full day');
expect(totalXP, lessThan(5000), reason: 'XP should be reasonable, not inflated');
print('✅ XP calculations appear correct: $totalXP total XP');
}
+7
View File
@@ -0,0 +1,7 @@
import 'package:test/test.dart';
void main() {
test('placeholder test', () {
expect(1 + 1, equals(2));
});
}