xp_nix/test/hyprland_config_parser_test.dart

618 lines
17 KiB
Dart

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'));
});
});
});
}