1use crate::{Error, NativeTheme, Result};
11use std::path::Path;
12
13const KDE_BREEZE_TOML: &str = include_str!("presets/kde-breeze.toml");
15const ADWAITA_TOML: &str = include_str!("presets/adwaita.toml");
16const WINDOWS_11_TOML: &str = include_str!("presets/windows-11.toml");
17const MACOS_SONOMA_TOML: &str = include_str!("presets/macos-sonoma.toml");
18const MATERIAL_TOML: &str = include_str!("presets/material.toml");
19const IOS_TOML: &str = include_str!("presets/ios.toml");
20const CATPPUCCIN_LATTE_TOML: &str = include_str!("presets/catppuccin-latte.toml");
21const CATPPUCCIN_FRAPPE_TOML: &str = include_str!("presets/catppuccin-frappe.toml");
22const CATPPUCCIN_MACCHIATO_TOML: &str = include_str!("presets/catppuccin-macchiato.toml");
23const CATPPUCCIN_MOCHA_TOML: &str = include_str!("presets/catppuccin-mocha.toml");
24const NORD_TOML: &str = include_str!("presets/nord.toml");
25const DRACULA_TOML: &str = include_str!("presets/dracula.toml");
26const GRUVBOX_TOML: &str = include_str!("presets/gruvbox.toml");
27const SOLARIZED_TOML: &str = include_str!("presets/solarized.toml");
28const TOKYO_NIGHT_TOML: &str = include_str!("presets/tokyo-night.toml");
29const ONE_DARK_TOML: &str = include_str!("presets/one-dark.toml");
30
31const KDE_BREEZE_LIVE_TOML: &str = include_str!("presets/kde-breeze-live.toml");
33const ADWAITA_LIVE_TOML: &str = include_str!("presets/adwaita-live.toml");
34const MACOS_SONOMA_LIVE_TOML: &str = include_str!("presets/macos-sonoma-live.toml");
35const WINDOWS_11_LIVE_TOML: &str = include_str!("presets/windows-11-live.toml");
36
37const PRESET_NAMES: &[&str] = &[
39 "kde-breeze",
40 "adwaita",
41 "windows-11",
42 "macos-sonoma",
43 "material",
44 "ios",
45 "catppuccin-latte",
46 "catppuccin-frappe",
47 "catppuccin-macchiato",
48 "catppuccin-mocha",
49 "nord",
50 "dracula",
51 "gruvbox",
52 "solarized",
53 "tokyo-night",
54 "one-dark",
55];
56
57pub(crate) fn preset(name: &str) -> Result<NativeTheme> {
58 let toml_str = match name {
59 "kde-breeze" => KDE_BREEZE_TOML,
60 "adwaita" => ADWAITA_TOML,
61 "windows-11" => WINDOWS_11_TOML,
62 "macos-sonoma" => MACOS_SONOMA_TOML,
63 "material" => MATERIAL_TOML,
64 "ios" => IOS_TOML,
65 "catppuccin-latte" => CATPPUCCIN_LATTE_TOML,
66 "catppuccin-frappe" => CATPPUCCIN_FRAPPE_TOML,
67 "catppuccin-macchiato" => CATPPUCCIN_MACCHIATO_TOML,
68 "catppuccin-mocha" => CATPPUCCIN_MOCHA_TOML,
69 "nord" => NORD_TOML,
70 "dracula" => DRACULA_TOML,
71 "gruvbox" => GRUVBOX_TOML,
72 "solarized" => SOLARIZED_TOML,
73 "tokyo-night" => TOKYO_NIGHT_TOML,
74 "one-dark" => ONE_DARK_TOML,
75 "kde-breeze-live" => KDE_BREEZE_LIVE_TOML,
77 "adwaita-live" => ADWAITA_LIVE_TOML,
78 "macos-sonoma-live" => MACOS_SONOMA_LIVE_TOML,
79 "windows-11-live" => WINDOWS_11_LIVE_TOML,
80 _ => return Err(Error::Unavailable(format!("unknown preset: {name}"))),
81 };
82 from_toml(toml_str)
83}
84
85pub(crate) fn list_presets() -> &'static [&'static str] {
86 PRESET_NAMES
87}
88
89const PLATFORM_SPECIFIC: &[(&str, &[&str])] = &[
91 ("kde-breeze", &["linux-kde"]),
92 ("adwaita", &["linux"]),
93 ("windows-11", &["windows"]),
94 ("macos-sonoma", &["macos"]),
95 ("ios", &["macos", "ios"]),
96];
97
98#[allow(unreachable_code)]
102fn detect_platform() -> &'static str {
103 #[cfg(target_os = "macos")]
104 {
105 return "macos";
106 }
107 #[cfg(target_os = "windows")]
108 {
109 return "windows";
110 }
111 #[cfg(target_os = "linux")]
112 {
113 let desktop = std::env::var("XDG_CURRENT_DESKTOP").unwrap_or_default();
114 for component in desktop.split(':') {
115 if component == "KDE" {
116 return "linux-kde";
117 }
118 }
119 "linux"
120 }
121 #[cfg(not(any(target_os = "linux", target_os = "windows", target_os = "macos")))]
122 {
123 "linux"
124 }
125}
126
127pub(crate) fn list_presets_for_platform() -> Vec<&'static str> {
132 let platform = detect_platform();
133
134 PRESET_NAMES
135 .iter()
136 .filter(|name| {
137 if let Some((_, platforms)) = PLATFORM_SPECIFIC.iter().find(|(n, _)| n == *name) {
138 platforms.iter().any(|p| platform.starts_with(p))
139 } else {
140 true }
142 })
143 .copied()
144 .collect()
145}
146
147pub(crate) fn from_toml(toml_str: &str) -> Result<NativeTheme> {
148 let theme: NativeTheme = toml::from_str(toml_str)?;
149 Ok(theme)
150}
151
152pub(crate) fn from_file(path: impl AsRef<Path>) -> Result<NativeTheme> {
153 let contents = std::fs::read_to_string(path)?;
154 from_toml(&contents)
155}
156
157pub(crate) fn to_toml(theme: &NativeTheme) -> Result<String> {
158 let s = toml::to_string_pretty(theme)?;
159 Ok(s)
160}
161
162#[cfg(test)]
163#[allow(clippy::unwrap_used, clippy::expect_used)]
164mod tests {
165 use super::*;
166
167 #[test]
168 fn all_presets_loadable_via_preset_fn() {
169 for name in list_presets() {
170 let theme =
171 preset(name).unwrap_or_else(|e| panic!("preset '{name}' failed to parse: {e}"));
172 assert!(
173 theme.light.is_some(),
174 "preset '{name}' missing light variant"
175 );
176 assert!(theme.dark.is_some(), "preset '{name}' missing dark variant");
177 }
178 }
179
180 #[test]
181 fn all_presets_have_nonempty_core_colors() {
182 for name in list_presets() {
183 let theme = preset(name).unwrap();
184 let light = theme.light.as_ref().unwrap();
185 let dark = theme.dark.as_ref().unwrap();
186
187 assert!(
188 light.defaults.accent.is_some(),
189 "preset '{name}' light missing accent"
190 );
191 assert!(
192 light.defaults.background.is_some(),
193 "preset '{name}' light missing background"
194 );
195 assert!(
196 light.defaults.foreground.is_some(),
197 "preset '{name}' light missing foreground"
198 );
199 assert!(
200 dark.defaults.accent.is_some(),
201 "preset '{name}' dark missing accent"
202 );
203 assert!(
204 dark.defaults.background.is_some(),
205 "preset '{name}' dark missing background"
206 );
207 assert!(
208 dark.defaults.foreground.is_some(),
209 "preset '{name}' dark missing foreground"
210 );
211 }
212 }
213
214 #[test]
215 fn preset_unknown_name_returns_unavailable() {
216 let err = preset("nonexistent").unwrap_err();
217 match err {
218 Error::Unavailable(msg) => assert!(msg.contains("nonexistent")),
219 other => panic!("expected Unavailable, got: {other:?}"),
220 }
221 }
222
223 #[test]
224 fn list_presets_returns_all_sixteen() {
225 let names = list_presets();
226 assert_eq!(names.len(), 16);
227 assert!(names.contains(&"kde-breeze"));
228 assert!(names.contains(&"adwaita"));
229 assert!(names.contains(&"windows-11"));
230 assert!(names.contains(&"macos-sonoma"));
231 assert!(names.contains(&"material"));
232 assert!(names.contains(&"ios"));
233 assert!(names.contains(&"catppuccin-latte"));
234 assert!(names.contains(&"catppuccin-frappe"));
235 assert!(names.contains(&"catppuccin-macchiato"));
236 assert!(names.contains(&"catppuccin-mocha"));
237 assert!(names.contains(&"nord"));
238 assert!(names.contains(&"dracula"));
239 assert!(names.contains(&"gruvbox"));
240 assert!(names.contains(&"solarized"));
241 assert!(names.contains(&"tokyo-night"));
242 assert!(names.contains(&"one-dark"));
243 }
244
245 #[test]
246 fn from_toml_minimal_valid() {
247 let toml_str = r##"
248name = "Minimal"
249
250[light.defaults]
251accent = "#ff0000"
252"##;
253 let theme = from_toml(toml_str).unwrap();
254 assert_eq!(theme.name, "Minimal");
255 assert!(theme.light.is_some());
256 let light = theme.light.unwrap();
257 assert_eq!(light.defaults.accent, Some(crate::Rgba::rgb(255, 0, 0)));
258 }
259
260 #[test]
261 fn from_toml_invalid_returns_format_error() {
262 let err = from_toml("{{{{invalid toml").unwrap_err();
263 match err {
264 Error::Format(_) => {}
265 other => panic!("expected Format, got: {other:?}"),
266 }
267 }
268
269 #[test]
270 fn to_toml_produces_valid_round_trip() {
271 let theme = preset("catppuccin-mocha").unwrap();
272 let toml_str = to_toml(&theme).unwrap();
273
274 let reparsed = from_toml(&toml_str).unwrap();
276 assert_eq!(reparsed.name, theme.name);
277 assert!(reparsed.light.is_some());
278 assert!(reparsed.dark.is_some());
279
280 let orig_light = theme.light.as_ref().unwrap();
282 let new_light = reparsed.light.as_ref().unwrap();
283 assert_eq!(orig_light.defaults.accent, new_light.defaults.accent);
284 }
285
286 #[test]
287 fn from_file_with_tempfile() {
288 let dir = std::env::temp_dir();
289 let path = dir.join("native_theme_test_preset.toml");
290 let toml_str = r##"
291name = "File Test"
292
293[light.defaults]
294accent = "#00ff00"
295"##;
296 std::fs::write(&path, toml_str).unwrap();
297
298 let theme = from_file(&path).unwrap();
299 assert_eq!(theme.name, "File Test");
300 assert!(theme.light.is_some());
301
302 let _ = std::fs::remove_file(&path);
304 }
305
306 #[test]
309 fn icon_set_native_presets_have_correct_values() {
310 let cases: &[(&str, &str)] = &[
311 ("windows-11", "segoe-fluent"),
312 ("macos-sonoma", "sf-symbols"),
313 ("ios", "sf-symbols"),
314 ("adwaita", "freedesktop"),
315 ("kde-breeze", "freedesktop"),
316 ("material", "material"),
317 ];
318 for (name, expected) in cases {
319 let theme = preset(name).unwrap();
320 let light = theme.light.as_ref().unwrap();
321 assert_eq!(
322 light.icon_set.as_deref(),
323 Some(*expected),
324 "preset '{name}' light.icon_set should be Some(\"{expected}\")"
325 );
326 let dark = theme.dark.as_ref().unwrap();
327 assert_eq!(
328 dark.icon_set.as_deref(),
329 Some(*expected),
330 "preset '{name}' dark.icon_set should be Some(\"{expected}\")"
331 );
332 }
333 }
334
335 #[test]
336 fn icon_set_community_presets_are_freedesktop() {
337 let community = &[
338 "catppuccin-latte",
339 "catppuccin-frappe",
340 "catppuccin-macchiato",
341 "catppuccin-mocha",
342 "nord",
343 "dracula",
344 "gruvbox",
345 "solarized",
346 "tokyo-night",
347 "one-dark",
348 ];
349 for name in community {
350 let theme = preset(name).unwrap();
351 let light = theme.light.as_ref().unwrap();
352 assert_eq!(
353 light.icon_set.as_deref(),
354 Some("freedesktop"),
355 "preset '{name}' light.icon_set should be Some(\"freedesktop\")"
356 );
357 let dark = theme.dark.as_ref().unwrap();
358 assert_eq!(
359 dark.icon_set.as_deref(),
360 Some("freedesktop"),
361 "preset '{name}' dark.icon_set should be Some(\"freedesktop\")"
362 );
363 }
364 }
365
366 #[test]
367 fn from_file_nonexistent_returns_error() {
368 let err = from_file("/tmp/nonexistent_theme_file_12345.toml").unwrap_err();
369 match err {
370 Error::Unavailable(_) => {}
371 other => panic!("expected Unavailable, got: {other:?}"),
372 }
373 }
374
375 #[test]
376 fn preset_names_match_list() {
377 for name in list_presets() {
379 assert!(preset(name).is_ok(), "preset '{name}' not loadable");
380 }
381 }
382
383 #[test]
384 fn presets_have_correct_names() {
385 assert_eq!(preset("kde-breeze").unwrap().name, "KDE Breeze");
386 assert_eq!(preset("adwaita").unwrap().name, "Adwaita");
387 assert_eq!(preset("windows-11").unwrap().name, "Windows 11");
388 assert_eq!(preset("macos-sonoma").unwrap().name, "macOS Sonoma");
389 assert_eq!(preset("material").unwrap().name, "Material");
390 assert_eq!(preset("ios").unwrap().name, "iOS");
391 assert_eq!(preset("catppuccin-latte").unwrap().name, "Catppuccin Latte");
392 assert_eq!(
393 preset("catppuccin-frappe").unwrap().name,
394 "Catppuccin Frappe"
395 );
396 assert_eq!(
397 preset("catppuccin-macchiato").unwrap().name,
398 "Catppuccin Macchiato"
399 );
400 assert_eq!(preset("catppuccin-mocha").unwrap().name, "Catppuccin Mocha");
401 assert_eq!(preset("nord").unwrap().name, "Nord");
402 assert_eq!(preset("dracula").unwrap().name, "Dracula");
403 assert_eq!(preset("gruvbox").unwrap().name, "Gruvbox");
404 assert_eq!(preset("solarized").unwrap().name, "Solarized");
405 assert_eq!(preset("tokyo-night").unwrap().name, "Tokyo Night");
406 assert_eq!(preset("one-dark").unwrap().name, "One Dark");
407 }
408
409 #[test]
410 fn all_presets_with_fonts_have_valid_sizes() {
411 for name in list_presets() {
412 let theme = preset(name).unwrap();
413 for (label, variant) in [
414 ("light", theme.light.as_ref()),
415 ("dark", theme.dark.as_ref()),
416 ] {
417 let variant = variant.unwrap();
418 if let Some(size) = variant.defaults.font.size {
420 assert!(
421 size > 0.0,
422 "preset '{name}' {label} font size must be positive, got {size}"
423 );
424 }
425 if let Some(mono_size) = variant.defaults.mono_font.size {
426 assert!(
427 mono_size > 0.0,
428 "preset '{name}' {label} mono font size must be positive, got {mono_size}"
429 );
430 }
431 }
432 }
433 }
434
435 #[test]
436 fn platform_presets_no_derived_fields() {
437 let platform_presets = &["kde-breeze", "adwaita", "windows-11", "macos-sonoma"];
439 for name in platform_presets {
440 let theme = preset(name).unwrap();
441 for (label, variant_opt) in [
442 ("light", theme.light.as_ref()),
443 ("dark", theme.dark.as_ref()),
444 ] {
445 let variant = variant_opt.unwrap();
446 assert!(
448 variant.button.primary_bg.is_none(),
449 "preset '{name}' {label}.button.primary_bg should be None (derived)"
450 );
451 assert!(
453 variant.checkbox.checked_bg.is_none(),
454 "preset '{name}' {label}.checkbox.checked_bg should be None (derived)"
455 );
456 assert!(
458 variant.slider.fill.is_none(),
459 "preset '{name}' {label}.slider.fill should be None (derived)"
460 );
461 assert!(
463 variant.progress_bar.fill.is_none(),
464 "preset '{name}' {label}.progress_bar.fill should be None (derived)"
465 );
466 assert!(
468 variant.switch.checked_bg.is_none(),
469 "preset '{name}' {label}.switch.checked_bg should be None (derived)"
470 );
471 }
472 }
473 }
474
475 #[test]
478 fn all_presets_resolve_validate() {
479 for name in list_presets() {
480 let theme = preset(name).unwrap();
481 if let Some(mut light) = theme.light.clone() {
482 light.resolve();
483 light.validate().unwrap_or_else(|e| {
484 panic!("preset {name} light variant failed validation: {e}");
485 });
486 }
487 if let Some(mut dark) = theme.dark.clone() {
488 dark.resolve();
489 dark.validate().unwrap_or_else(|e| {
490 panic!("preset {name} dark variant failed validation: {e}");
491 });
492 }
493 }
494 }
495
496 #[test]
497 fn resolve_fills_accent_derived_fields() {
498 let theme = preset("catppuccin-mocha").unwrap();
501 let mut light = theme.light.clone().unwrap();
502
503 assert!(
505 light.button.primary_bg.is_none(),
506 "primary_bg should be None pre-resolve"
507 );
508 assert!(
509 light.checkbox.checked_bg.is_none(),
510 "checkbox.checked_bg should be None pre-resolve"
511 );
512 assert!(
513 light.slider.fill.is_none(),
514 "slider.fill should be None pre-resolve"
515 );
516 assert!(
517 light.progress_bar.fill.is_none(),
518 "progress_bar.fill should be None pre-resolve"
519 );
520 assert!(
521 light.switch.checked_bg.is_none(),
522 "switch.checked_bg should be None pre-resolve"
523 );
524
525 light.resolve();
526
527 let accent = light.defaults.accent.unwrap();
529 assert_eq!(
530 light.button.primary_bg,
531 Some(accent),
532 "button.primary_bg should match accent"
533 );
534 assert_eq!(
535 light.checkbox.checked_bg,
536 Some(accent),
537 "checkbox.checked_bg should match accent"
538 );
539 assert_eq!(
540 light.slider.fill,
541 Some(accent),
542 "slider.fill should match accent"
543 );
544 assert_eq!(
545 light.progress_bar.fill,
546 Some(accent),
547 "progress_bar.fill should match accent"
548 );
549 assert_eq!(
550 light.switch.checked_bg,
551 Some(accent),
552 "switch.checked_bg should match accent"
553 );
554 }
555
556 #[test]
557 fn resolve_then_validate_produces_complete_theme() {
558 let theme = preset("catppuccin-mocha").unwrap();
559 let mut light = theme.light.clone().unwrap();
560 light.resolve();
561 let resolved = light.validate().unwrap();
562
563 assert_eq!(resolved.defaults.font.family, "Inter");
564 assert_eq!(resolved.defaults.font.size, 14.0);
565 assert_eq!(resolved.defaults.font.weight, 400);
566 assert_eq!(resolved.defaults.line_height, 1.4);
567 assert_eq!(resolved.defaults.radius, 8.0);
568 assert_eq!(resolved.defaults.focus_ring_width, 2.0);
569 assert_eq!(resolved.defaults.icon_sizes.toolbar, 24.0);
570 assert_eq!(resolved.defaults.text_scaling_factor, 1.0);
571 assert!(!resolved.defaults.reduce_motion);
572 assert_eq!(resolved.window.background, resolved.defaults.background);
574 assert_eq!(resolved.icon_set, "freedesktop");
576 }
577
578 #[test]
579 fn font_subfield_inheritance_integration() {
580 let theme = preset("catppuccin-mocha").unwrap();
583 let mut light = theme.light.clone().unwrap();
584
585 use crate::model::FontSpec;
587 light.menu.font = Some(FontSpec {
588 family: None,
589 size: Some(12.0),
590 weight: None,
591 });
592
593 light.resolve();
594 let resolved = light.validate().unwrap();
595
596 assert_eq!(
598 resolved.menu.font.family, "Inter",
599 "menu font family should inherit from defaults"
600 );
601 assert_eq!(
602 resolved.menu.font.size, 12.0,
603 "menu font size should be the explicit value"
604 );
605 assert_eq!(
606 resolved.menu.font.weight, 400,
607 "menu font weight should inherit from defaults"
608 );
609 }
610
611 #[test]
612 fn text_scale_inheritance_integration() {
613 let theme = preset("catppuccin-mocha").unwrap();
615 let mut light = theme.light.clone().unwrap();
616
617 light.text_scale.caption = None;
619
620 light.resolve();
621 let resolved = light.validate().unwrap();
622
623 assert_eq!(
625 resolved.text_scale.caption.size, 14.0,
626 "caption size from defaults.font.size"
627 );
628 assert_eq!(
629 resolved.text_scale.caption.weight, 400,
630 "caption weight from defaults.font.weight"
631 );
632 assert!(
634 (resolved.text_scale.caption.line_height - 19.6).abs() < 0.01,
635 "caption line_height should be line_height_multiplier * size = 19.6, got {}",
636 resolved.text_scale.caption.line_height
637 );
638 }
639
640 #[test]
641 fn all_presets_round_trip_exact() {
642 for name in list_presets() {
644 let theme1 =
645 preset(name).unwrap_or_else(|e| panic!("preset '{name}' failed to parse: {e}"));
646 let toml_str = to_toml(&theme1)
647 .unwrap_or_else(|e| panic!("preset '{name}' failed to serialize: {e}"));
648 let theme2 = from_toml(&toml_str)
649 .unwrap_or_else(|e| panic!("preset '{name}' failed to re-parse: {e}"));
650 assert_eq!(
651 theme1, theme2,
652 "preset '{name}' round-trip produced different value"
653 );
654 }
655 }
656
657 #[test]
660 fn live_presets_loadable() {
661 let live_names = &[
662 "kde-breeze-live",
663 "adwaita-live",
664 "macos-sonoma-live",
665 "windows-11-live",
666 ];
667 for name in live_names {
668 let theme = preset(name)
669 .unwrap_or_else(|e| panic!("live preset '{name}' failed to parse: {e}"));
670
671 assert!(
673 theme.light.is_some(),
674 "live preset '{name}' missing light variant"
675 );
676 assert!(
677 theme.dark.is_some(),
678 "live preset '{name}' missing dark variant"
679 );
680
681 let light = theme.light.as_ref().unwrap();
682 let dark = theme.dark.as_ref().unwrap();
683
684 assert!(
686 light.defaults.accent.is_none(),
687 "live preset '{name}' light should have no accent"
688 );
689 assert!(
690 light.defaults.background.is_none(),
691 "live preset '{name}' light should have no background"
692 );
693 assert!(
694 light.defaults.foreground.is_none(),
695 "live preset '{name}' light should have no foreground"
696 );
697 assert!(
698 dark.defaults.accent.is_none(),
699 "live preset '{name}' dark should have no accent"
700 );
701 assert!(
702 dark.defaults.background.is_none(),
703 "live preset '{name}' dark should have no background"
704 );
705 assert!(
706 dark.defaults.foreground.is_none(),
707 "live preset '{name}' dark should have no foreground"
708 );
709
710 assert!(
712 light.defaults.font.family.is_none(),
713 "live preset '{name}' light should have no font family"
714 );
715 assert!(
716 light.defaults.font.size.is_none(),
717 "live preset '{name}' light should have no font size"
718 );
719 assert!(
720 light.defaults.font.weight.is_none(),
721 "live preset '{name}' light should have no font weight"
722 );
723 assert!(
724 dark.defaults.font.family.is_none(),
725 "live preset '{name}' dark should have no font family"
726 );
727 assert!(
728 dark.defaults.font.size.is_none(),
729 "live preset '{name}' dark should have no font size"
730 );
731 assert!(
732 dark.defaults.font.weight.is_none(),
733 "live preset '{name}' dark should have no font weight"
734 );
735 }
736 }
737
738 #[test]
739 fn list_presets_for_platform_returns_subset() {
740 let all = list_presets();
741 let filtered = list_presets_for_platform();
742 for name in &filtered {
744 assert!(
745 all.contains(name),
746 "filtered preset '{name}' not in full list"
747 );
748 }
749 let community = &[
751 "catppuccin-latte",
752 "catppuccin-frappe",
753 "catppuccin-macchiato",
754 "catppuccin-mocha",
755 "nord",
756 "dracula",
757 "gruvbox",
758 "solarized",
759 "tokyo-night",
760 "one-dark",
761 ];
762 for name in community {
763 assert!(
764 filtered.contains(name),
765 "community preset '{name}' should always be in filtered list"
766 );
767 }
768 assert!(
770 filtered.contains(&"material"),
771 "material should always be in filtered list"
772 );
773 }
774
775 #[test]
776 fn live_presets_fail_validate_standalone() {
777 let live_names = &[
778 "kde-breeze-live",
779 "adwaita-live",
780 "macos-sonoma-live",
781 "windows-11-live",
782 ];
783 for name in live_names {
784 let theme = preset(name).unwrap();
785 let mut light = theme.light.clone().unwrap();
786 light.resolve();
787 let result = light.validate();
788 assert!(
789 result.is_err(),
790 "live preset '{name}' light should fail validation standalone (missing colors/fonts)"
791 );
792
793 let mut dark = theme.dark.clone().unwrap();
794 dark.resolve();
795 let result = dark.validate();
796 assert!(
797 result.is_err(),
798 "live preset '{name}' dark should fail validation standalone (missing colors/fonts)"
799 );
800 }
801 }
802}