use opaline::OpalineColor;
use opaline::builtins;
use opaline::schema::ThemeVariant;
use pretty_assertions::assert_eq;
use std::fs;
#[cfg(all(
feature = "builtin-themes",
feature = "discovery",
feature = "global-state"
))]
use std::sync::{Mutex, MutexGuard, OnceLock};
#[cfg(all(
feature = "builtin-themes",
feature = "discovery",
feature = "global-state"
))]
fn global_lock() -> MutexGuard<'static, ()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
LOCK.get_or_init(|| Mutex::new(())).lock().expect("lock")
}
#[test]
fn silkcircuit_neon_loads() {
let theme = builtins::silkcircuit_neon();
assert_eq!(theme.meta.name, "SilkCircuit Neon");
assert_eq!(theme.meta.variant, ThemeVariant::Dark);
assert_eq!(theme.meta.author.as_deref(), Some("hyperb1iss"));
}
#[test]
fn silkcircuit_neon_accent_colors() {
let theme = builtins::silkcircuit_neon();
assert_eq!(
theme.color("accent.primary"),
OpalineColor::new(225, 53, 255)
);
assert_eq!(
theme.color("accent.secondary"),
OpalineColor::new(128, 255, 234)
);
}
#[test]
fn silkcircuit_neon_keyword_style_is_bold_purple() {
let theme = builtins::silkcircuit_neon();
let kw = theme.style("keyword");
assert_eq!(kw.fg, Some(OpalineColor::new(225, 53, 255)));
assert!(kw.bold);
}
#[test]
fn silkcircuit_neon_has_gradients() {
let theme = builtins::silkcircuit_neon();
assert!(theme.has_gradient("primary"));
assert!(theme.has_gradient("warm"));
assert!(theme.has_gradient("aurora"));
}
#[test]
fn silkcircuit_neon_primary_gradient_endpoints() {
let theme = builtins::silkcircuit_neon();
assert_eq!(
theme.gradient("primary", 0.0),
OpalineColor::new(225, 53, 255)
);
assert_eq!(
theme.gradient("primary", 1.0),
OpalineColor::new(128, 255, 234)
);
}
#[test]
fn all_builtins_load() {
for &(id, display) in builtins::builtin_names() {
let theme = builtins::load_by_name(id)
.unwrap_or_else(|| panic!("builtin '{id}' ({display}) failed to load"));
assert_eq!(theme.meta.name, display, "display name mismatch for {id}");
}
}
#[test]
fn builtin_count_is_39() {
assert_eq!(builtins::builtin_names().len(), builtins::BUILTIN_COUNT);
assert_eq!(builtins::BUILTIN_COUNT, 39);
}
const REQUIRED_TOKENS: &[&str] = &[
"text.primary",
"text.secondary",
"text.muted",
"text.dim",
"bg.base",
"bg.panel",
"bg.code",
"bg.highlight",
"bg.selection",
"accent.primary",
"accent.secondary",
"accent.tertiary",
"accent.deep",
"success",
"error",
"warning",
"info",
"border.focused",
"border.unfocused",
"code.keyword",
"code.function",
"code.string",
"code.number",
"code.comment",
"code.type",
"code.line_number",
];
#[test]
fn all_builtins_have_required_tokens() {
for &(id, _) in builtins::builtin_names() {
let theme = builtins::load_by_name(id).expect("loads");
for &token in REQUIRED_TOKENS {
assert!(
theme.has_token(token),
"theme '{id}' missing required token: {token}"
);
}
}
}
const REQUIRED_STYLES: &[&str] = &[
"keyword",
"line_number",
"selected",
"active_selected",
"focused_border",
"unfocused_border",
"success_style",
"error_style",
"warning_style",
"info_style",
"dimmed",
"muted",
"inline_code",
];
const FORBIDDEN_LEGACY_TOKENS: &[&str] = &[
"git.staged",
"git.modified",
"git.untracked",
"git.deleted",
"diff.added",
"diff.removed",
"diff.hunk",
"diff.context",
"code.hash",
"code.path",
"mode.active",
"mode.inactive",
"mode.hover",
];
const FORBIDDEN_LEGACY_STYLES: &[&str] = &[
"file_path",
"file_path_bold",
"commit_hash",
"mode_active",
"mode_inactive",
"mode_hover",
"git_staged",
"git_modified",
"git_untracked",
"git_deleted",
"diff_added",
"diff_removed",
"diff_hunk",
"diff_context",
"author",
"timestamp",
];
#[test]
fn all_builtins_have_required_styles() {
for &(id, _) in builtins::builtin_names() {
let theme = builtins::load_by_name(id).expect("loads");
for &style in REQUIRED_STYLES {
assert!(
theme.has_style(style),
"theme '{id}' missing required style: {style}"
);
}
}
}
#[test]
fn builtins_do_not_embed_legacy_app_tokens_or_styles() {
for &(id, _) in builtins::builtin_names() {
let theme = builtins::load_by_name(id).expect("loads");
for &token in FORBIDDEN_LEGACY_TOKENS {
assert!(
!theme.has_token(token),
"theme '{id}' should not embed legacy app token: {token}"
);
}
for &style in FORBIDDEN_LEGACY_STYLES {
assert!(
!theme.has_style(style),
"theme '{id}' should not embed legacy app style: {style}"
);
}
}
}
const REQUIRED_GRADIENTS: &[&str] = &[
"primary",
"warm",
"success_gradient",
"error_gradient",
"aurora",
];
#[test]
fn all_builtins_have_required_gradients() {
for &(id, _) in builtins::builtin_names() {
let theme = builtins::load_by_name(id).expect("loads");
for &gradient in REQUIRED_GRADIENTS {
assert!(
theme.has_gradient(gradient),
"theme '{id}' missing required gradient: {gradient}"
);
}
}
}
#[test]
fn light_themes_marked_correctly() {
let light_ids = [
"catppuccin-latte",
"solarized-light",
"silkcircuit-dawn",
"github-light",
"ayu-light",
"light-owl",
"flexoki-light",
];
for id in &light_ids {
let theme = builtins::load_by_name(id).expect("loads");
assert!(
theme.is_light(),
"theme '{id}' should be ThemeVariant::Light"
);
}
}
#[test]
fn dark_themes_marked_correctly() {
let dark_ids = [
"silkcircuit-neon",
"silkcircuit-soft",
"silkcircuit-glow",
"silkcircuit-vibrant",
"catppuccin-mocha",
"dracula",
"nord",
"tokyo-night",
"gruvbox-dark",
"one-dark",
"github-dark-dimmed",
"monokai-pro",
"ayu-dark",
"ayu-mirage",
"night-owl",
"palenight",
"flexoki-dark",
];
for id in &dark_ids {
let theme = builtins::load_by_name(id).expect("loads");
assert!(theme.is_dark(), "theme '{id}' should be ThemeVariant::Dark");
}
}
#[test]
fn load_by_name_default_alias() {
let default = builtins::load_by_name("default").expect("default loads");
let neon = builtins::load_by_name("silkcircuit-neon").expect("neon loads");
assert_eq!(default.meta.name, neon.meta.name);
}
#[test]
fn load_by_name_unknown() {
assert!(builtins::load_by_name("nonexistent-theme").is_none());
}
#[test]
fn list_available_themes_keeps_builtin_ids_unique() {
let themes = builtins::list_available_themes();
for &(id, _) in builtins::builtin_names() {
let visible_count = themes.iter().filter(|theme| theme.name == id).count();
assert_eq!(
visible_count, 1,
"expected exactly one visible theme entry for builtin id {id}"
);
}
}
#[test]
fn default_theme_is_silkcircuit_neon() {
let theme = opaline::Theme::default();
assert_eq!(theme.meta.name, "SilkCircuit Neon");
}
#[test]
fn theme_info_load_prefers_path_over_builtin_id() {
let unique = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("time went backwards")
.as_nanos();
let temp_dir = std::env::temp_dir().join(format!("opaline-theme-info-{unique}"));
fs::create_dir_all(&temp_dir).expect("create temp dir");
let path = temp_dir.join("dracula.toml");
fs::write(
&path,
r##"
[meta]
name = "Shadow Dracula"
variant = "dark"
author = "local"
[palette]
shadow = "#010203"
"##,
)
.expect("write theme file");
let info = builtins::ThemeInfo {
name: "dracula".to_string(),
display_name: "Shadow Dracula".to_string(),
variant: ThemeVariant::Dark,
author: "local".to_string(),
description: String::new(),
builtin: false,
path: Some(path),
};
let theme = info.load().expect("theme loads");
assert_eq!(theme.meta.name, "Shadow Dracula");
}
#[cfg(all(
feature = "builtin-themes",
feature = "discovery",
feature = "global-state"
))]
#[test]
fn discovered_theme_shadowing_builtin_wins_in_loader_and_listing() {
let _guard = global_lock();
let previous = opaline::current();
let unique = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("time went backwards")
.as_nanos();
let temp_root = std::env::temp_dir().join(format!("opaline-config-{unique}"));
fs::create_dir_all(&temp_root).expect("create temp config root");
let base_dir = temp_root.join("opaline").join("themes");
let app_dir = temp_root.join("opaline-local").join("themes");
fs::create_dir_all(&base_dir).expect("create base theme dir");
fs::create_dir_all(&app_dir).expect("create app theme dir");
let builtin = builtins::load_by_name("dracula").expect("builtin loads");
assert_eq!(builtin.meta.name, "Dracula");
fs::write(
base_dir.join("dracula.toml"),
r##"
[meta]
name = "Shadow Dracula"
variant = "dark"
author = "local"
[palette]
shadow = "#010203"
"##,
)
.expect("write shadowing theme");
fs::write(
app_dir.join("opaline-local.toml"),
r##"
[meta]
name = "Opaline Local"
variant = "light"
author = "local"
[palette]
accent = "#abcdef"
"##,
)
.expect("write app theme");
opaline::load_theme_by_name_in_dirs("dracula", [base_dir.clone()]).expect("custom theme loads");
assert_eq!(opaline::current().meta.name, "Shadow Dracula");
let all = builtins::list_available_themes_in_dirs([base_dir.clone(), app_dir.clone()]);
let dracula = all
.iter()
.find(|theme| theme.name == "dracula")
.expect("dracula theme present");
assert!(!dracula.builtin);
assert_eq!(dracula.display_name, "Shadow Dracula");
assert!(dracula.path.is_some());
let app_themes = builtins::list_available_themes_in_dirs([app_dir.clone()]);
assert!(
app_themes
.iter()
.any(|theme| theme.name == "opaline-local" && theme.display_name == "Opaline Local")
);
let _ = fs::remove_file(base_dir.join("dracula.toml"));
let _ = fs::remove_file(app_dir.join("opaline-local.toml"));
opaline::set_theme((*previous).clone());
}
#[cfg(all(
feature = "builtin-themes",
feature = "discovery",
feature = "global-state"
))]
#[test]
fn later_discovery_dirs_override_earlier_ones() {
let _guard = global_lock();
let previous = opaline::current();
let unique = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("time went backwards")
.as_nanos();
let temp_root = std::env::temp_dir().join(format!("opaline-precedence-{unique}"));
let base_dir = temp_root.join("opaline").join("themes");
let app_dir = temp_root.join("opaline-local").join("themes");
fs::create_dir_all(&base_dir).expect("create base theme dir");
fs::create_dir_all(&app_dir).expect("create app theme dir");
fs::write(
base_dir.join("shared.toml"),
r##"
[meta]
name = "Base Shared"
variant = "dark"
[palette]
accent = "#010203"
"##,
)
.expect("write base theme");
fs::write(
app_dir.join("shared.toml"),
r##"
[meta]
name = "App Shared"
variant = "light"
[palette]
accent = "#abcdef"
"##,
)
.expect("write app theme");
opaline::load_theme_by_name_in_dirs("shared", [base_dir.clone(), app_dir.clone()])
.expect("later theme loads");
assert_eq!(opaline::current().meta.name, "App Shared");
let listed = builtins::list_available_themes_in_dirs([base_dir.clone(), app_dir.clone()]);
let shared = listed
.iter()
.find(|theme| theme.name == "shared")
.expect("shared theme present");
assert_eq!(shared.display_name, "App Shared");
let _ = fs::remove_file(base_dir.join("shared.toml"));
let _ = fs::remove_file(app_dir.join("shared.toml"));
opaline::set_theme((*previous).clone());
}