use css_variable_lsp::manager::CssVariableManager;
use css_variable_lsp::parsers::{parse_css_document, parse_html_document};
use css_variable_lsp::types::Config;
use ls_types::Uri;
use std::str::FromStr;
#[tokio::test]
async fn test_css_variable_full_workflow() {
let manager = CssVariableManager::new(Config::default());
let css_uri = Uri::from_str("file:///test.css").unwrap();
let html_uri = Uri::from_str("file:///test.html").unwrap();
let css_content = r#"
:root {
--primary-color: #3b82f6;
--secondary-color: #8b5cf6;
--spacing: 1rem;
}
.button {
background: var(--primary-color);
padding: var(--spacing);
}
.card {
color: var(--undefined-variable);
border-color: var(--secondary-color, #ccc);
}
"#;
parse_css_document(css_content, &css_uri, &manager)
.await
.unwrap();
let html_content = r#"
<html>
<head>
<style>
.header {
--header-bg: #fff;
background: var(--header-bg);
}
</style>
</head>
<body>
<div style="--inline-color: red; color: var(--inline-color);">
Test
</div>
</body>
</html>
"#;
parse_html_document(html_content, &html_uri, &manager)
.await
.unwrap();
let primary_defs = manager.get_variables("--primary-color").await;
assert_eq!(primary_defs.len(), 1);
assert_eq!(primary_defs[0].value.trim(), "#3b82f6");
assert_eq!(primary_defs[0].selector, ":root");
let header_defs = manager.get_variables("--header-bg").await;
assert_eq!(header_defs.len(), 1);
assert_eq!(header_defs[0].value.trim(), "#fff");
let inline_defs = manager.get_variables("--inline-color").await;
assert_eq!(inline_defs.len(), 1);
assert_eq!(inline_defs[0].value.trim(), "red");
assert!(inline_defs[0].inline);
let primary_usages = manager.get_usages("--primary-color").await;
assert_eq!(primary_usages.len(), 1);
assert_eq!(primary_usages[0].usage_context, ".button");
let undefined_usages = manager.get_usages("--undefined-variable").await;
assert_eq!(undefined_usages.len(), 1);
let (defs, usages) = manager.get_references("--secondary-color").await;
assert_eq!(defs.len(), 1);
assert_eq!(usages.len(), 1);
manager.remove_document(&css_uri).await;
let primary_after_removal = manager.get_variables("--primary-color").await;
assert_eq!(primary_after_removal.len(), 0);
let header_after_removal = manager.get_variables("--header-bg").await;
assert_eq!(header_after_removal.len(), 1);
}
#[tokio::test]
async fn test_cascade_ordering() {
let manager = CssVariableManager::new(Config::default());
let uri = Uri::from_str("file:///cascade.css").unwrap();
let css_content = r#"
:root {
--color: blue;
}
.class {
--color: green;
}
#id {
--color: red !important;
}
div {
--color: yellow;
}
"#;
parse_css_document(css_content, &uri, &manager)
.await
.unwrap();
let color_defs = manager.get_variables("--color").await;
assert_eq!(color_defs.len(), 4);
let selectors: Vec<String> = color_defs.iter().map(|d| d.selector.clone()).collect();
assert!(selectors.contains(&":root".to_string()));
assert!(selectors.contains(&".class".to_string()));
assert!(selectors.contains(&"#id".to_string()));
assert!(selectors.contains(&"div".to_string()));
let important_count = color_defs.iter().filter(|d| d.important).count();
assert_eq!(important_count, 1);
}
#[tokio::test]
async fn test_color_resolution_chain() {
let manager = CssVariableManager::new(Config::default());
let uri = Uri::from_str("file:///colors.css").unwrap();
let css_content = r#"
:root {
--base-color: #3b82f6;
--primary: var(--base-color);
--theme-color: var(--primary);
}
"#;
parse_css_document(css_content, &uri, &manager)
.await
.unwrap();
let base = manager.get_variables("--base-color").await;
assert_eq!(base.len(), 1);
assert_eq!(base[0].value.trim(), "#3b82f6");
let primary = manager.get_variables("--primary").await;
assert_eq!(primary.len(), 1);
assert!(primary[0].value.contains("var(--base-color)"));
let color = manager.resolve_variable_color("--theme-color").await;
assert!(color.is_some());
}
#[tokio::test]
async fn test_color_resolution_with_fallbacks() {
let manager = CssVariableManager::new(Config::default());
let uri = Uri::from_str("file:///fallbacks.css").unwrap();
let css_content = r#"
:root {
--base-color: #ff0000;
--alias-color: var(--base-color, #00ff00);
}
"#;
parse_css_document(css_content, &uri, &manager)
.await
.unwrap();
let color = manager.resolve_variable_color("--alias-color").await;
assert!(color.is_some());
let color = color.unwrap();
assert!((color.red - 1.0).abs() < 0.01);
assert!((color.green - 0.0).abs() < 0.01);
assert!((color.blue - 0.0).abs() < 0.01);
}
#[tokio::test]
async fn test_multiple_file_types() {
let manager = CssVariableManager::new(Config::default());
let css_uri = Uri::from_str("file:///styles.css").unwrap();
let css_content = ":root { --css-var: blue; }";
parse_css_document(css_content, &css_uri, &manager)
.await
.unwrap();
let html_uri = Uri::from_str("file:///index.html").unwrap();
let html_content = r#"
<style>:root { --html-var: red; }</style>
<div style="--inline-var: green;"></div>
"#;
parse_html_document(html_content, &html_uri, &manager)
.await
.unwrap();
let all_vars = manager.get_all_variables().await;
assert!(all_vars.len() >= 3);
let var_names: Vec<String> = all_vars.iter().map(|v| v.name.clone()).collect();
assert!(var_names.contains(&"--css-var".to_string()));
assert!(var_names.contains(&"--html-var".to_string()));
assert!(var_names.contains(&"--inline-var".to_string()));
}
#[tokio::test]
async fn test_var_fallback_handling() {
let manager = CssVariableManager::new(Config::default());
let uri = Uri::from_str("file:///fallback.css").unwrap();
let css_content = r#"
.button {
color: var(--primary-color, blue);
background: var(--bg-color, var(--fallback, #fff));
padding: var(--spacing);
}
"#;
parse_css_document(css_content, &uri, &manager)
.await
.unwrap();
let primary_usages = manager.get_usages("--primary-color").await;
assert_eq!(primary_usages.len(), 1);
let bg_usages = manager.get_usages("--bg-color").await;
assert_eq!(bg_usages.len(), 1);
let spacing_usages = manager.get_usages("--spacing").await;
assert_eq!(spacing_usages.len(), 1);
let fallback_usages = manager.get_usages("--fallback").await;
assert_eq!(fallback_usages.len(), 0);
}
#[tokio::test]
async fn test_css_variables_in_media_queries() {
let manager = CssVariableManager::new(Config::default());
let uri = Uri::from_str("file:///media.css").unwrap();
let css_content = r#"
:root {
--breakpoint-md: 768px;
--theme-color: blue;
}
@media (min-width: var(--breakpoint-md)) {
.responsive {
--responsive-padding: 2rem;
color: var(--theme-color);
}
}
@media screen and (max-width: 480px) {
.mobile {
--mobile-spacing: var(--responsive-padding, 1rem);
}
}
"#;
parse_css_document(css_content, &uri, &manager)
.await
.unwrap();
let responsive_defs = manager.get_variables("--responsive-padding").await;
assert_eq!(responsive_defs.len(), 1);
assert!(!responsive_defs[0].selector.is_empty());
let theme_usages = manager.get_usages("--theme-color").await;
assert_eq!(theme_usages.len(), 1);
let responsive_usages = manager.get_usages("--responsive-padding").await;
assert_eq!(responsive_usages.len(), 1);
}
#[tokio::test]
async fn test_css_variables_in_keyframes() {
let manager = CssVariableManager::new(Config::default());
let uri = Uri::from_str("file:///keyframes.css").unwrap();
let css_content = r#"
:root {
--animation-duration: 0.3s;
--start-color: red;
--end-color: blue;
}
@keyframes slide-in {
0% {
--current-color: var(--start-color);
transform: translateX(-100%);
}
100% {
--current-color: var(--end-color);
transform: translateX(0);
}
}
.animated {
animation: slide-in var(--animation-duration) ease-in-out;
color: var(--current-color, black);
}
"#;
parse_css_document(css_content, &uri, &manager)
.await
.unwrap();
let current_color_defs = manager.get_variables("--current-color").await;
assert_eq!(current_color_defs.len(), 2);
let start_color_usages = manager.get_usages("--start-color").await;
assert_eq!(start_color_usages.len(), 1);
let end_color_usages = manager.get_usages("--end-color").await;
assert_eq!(end_color_usages.len(), 1);
}
#[tokio::test]
async fn test_complex_css_selectors() {
let manager = CssVariableManager::new(Config::default());
let uri = Uri::from_str("file:///complex.css").unwrap();
let css_content = r#"
:root {
--primary: blue;
}
/* Complex selectors */
.component[data-theme="dark"] .header > h1:first-child,
.component[data-theme="light"] .header > h1:first-child {
--header-color: var(--primary);
color: var(--header-color);
}
.card:hover:not(.disabled)::before {
--border-color: #ccc;
border-color: var(--border-color);
}
/* Nested selectors with combinators */
.parent .child + .sibling {
--spacing: 1rem;
margin: var(--spacing);
}
"#;
parse_css_document(css_content, &uri, &manager)
.await
.unwrap();
let header_color_defs = manager.get_variables("--header-color").await;
assert_eq!(header_color_defs.len(), 1);
let border_color_defs = manager.get_variables("--border-color").await;
assert_eq!(border_color_defs.len(), 1);
let primary_usages = manager.get_usages("--primary").await;
assert_eq!(primary_usages.len(), 1);
}
#[tokio::test]
async fn test_css_variables_in_calc_expressions() {
let manager = CssVariableManager::new(Config::default());
let uri = Uri::from_str("file:///calc.css").unwrap();
let css_content = r#"
:root {
--base-spacing: 1rem;
--scale-factor: 1.5;
}
.element {
--computed-width: calc(100% - var(--base-spacing) * 2);
--scaled-size: calc(var(--base-spacing) * var(--scale-factor));
width: var(--computed-width);
font-size: var(--scaled-size);
}
.nested {
--complex-calc: calc(var(--base-spacing) + 0.5rem);
padding: var(--complex-calc);
}
"#;
parse_css_document(css_content, &uri, &manager)
.await
.unwrap();
let computed_defs = manager.get_variables("--computed-width").await;
assert_eq!(computed_defs.len(), 1);
assert!(computed_defs[0].value.contains("calc("));
let base_spacing_usages = manager.get_usages("--base-spacing").await;
assert_eq!(base_spacing_usages.len(), 3);
let scale_usages = manager.get_usages("--scale-factor").await;
assert_eq!(scale_usages.len(), 1);
}
#[tokio::test]
async fn test_css_variables_in_layout_properties() {
let manager = CssVariableManager::new(Config::default());
let uri = Uri::from_str("file:///layout.css").unwrap();
let css_content = r#"
:root {
--grid-columns: 1fr 2fr 1fr;
--flex-gap: 1rem;
--min-width: 200px;
}
.grid-container {
--grid-template: var(--grid-columns) / 1fr;
grid-template: var(--grid-template);
gap: var(--flex-gap);
}
.flex-container {
--flex-layout: row nowrap;
flex-flow: var(--flex-layout);
gap: var(--flex-gap);
}
.constrained {
--size-constraint: minmax(var(--min-width), 1fr);
grid-template-columns: var(--size-constraint);
}
"#;
parse_css_document(css_content, &uri, &manager)
.await
.unwrap();
let grid_template_defs = manager.get_variables("--grid-template").await;
assert_eq!(grid_template_defs.len(), 1);
let flex_layout_defs = manager.get_variables("--flex-layout").await;
assert_eq!(flex_layout_defs.len(), 1);
let grid_columns_usages = manager.get_usages("--grid-columns").await;
assert_eq!(grid_columns_usages.len(), 1);
let flex_gap_usages = manager.get_usages("--flex-gap").await;
assert_eq!(flex_gap_usages.len(), 2);
let min_width_usages = manager.get_usages("--min-width").await;
assert_eq!(min_width_usages.len(), 1);
}
#[tokio::test]
async fn test_css_custom_properties_edge_cases() {
let manager = CssVariableManager::new(Config::default());
let uri = Uri::from_str("file:///edge-cases.css").unwrap();
let css_content = r#"
:root {
--simple: blue;
--with-dashes: red;
--with_underscores: green;
--with-numbers123: yellow;
--mixed-123_abc: purple;
}
.test {
color: var(--simple);
background: var(--with-dashes);
border-color: var(--with_underscores);
outline-color: var(--with-numbers123);
box-shadow: 0 0 10px var(--mixed-123_abc);
}
/* Variables that might be confused with other syntax */
.special {
--calc-like: calc(100% - 20px);
--url-like: url(https://example.com);
--function-like: rgba(255, 0, 0, 0.5);
background: var(--calc-like) var(--url-like) var(--function-like);
}
"#;
parse_css_document(css_content, &uri, &manager)
.await
.unwrap();
let all_vars = manager.get_all_variables().await;
assert_eq!(all_vars.len(), 8);
let var_names: Vec<String> = all_vars.iter().map(|v| v.name.clone()).collect();
assert!(var_names.contains(&"--simple".to_string()));
assert!(var_names.contains(&"--with-dashes".to_string()));
assert!(var_names.contains(&"--with_underscores".to_string()));
assert!(var_names.contains(&"--with-numbers123".to_string()));
assert!(var_names.contains(&"--mixed-123_abc".to_string()));
let simple_usages = manager.get_usages("--simple").await;
assert_eq!(simple_usages.len(), 1);
let mixed_usages = manager.get_usages("--mixed-123_abc").await;
assert_eq!(mixed_usages.len(), 1);
}
#[tokio::test]
async fn test_lsp_rename_functionality() {
let manager = CssVariableManager::new(Config::default());
let uri = Uri::from_str("file:///rename-test.css").unwrap();
let css_content = r#"
:root {
--primary-color: #3b82f6;
--secondary-color: #8b5cf6;
}
.button {
background: var(--primary-color);
border: 1px solid var(--secondary-color);
}
.card {
color: var(--primary-color);
}
"#;
parse_css_document(css_content, &uri, &manager)
.await
.unwrap();
let primary_defs = manager.get_variables("--primary-color").await;
assert_eq!(primary_defs.len(), 1);
let def = &primary_defs[0];
assert!(def.name_range.is_some());
let name_range = def.name_range.as_ref().unwrap();
assert!(name_range.end.line >= name_range.start.line);
assert!(name_range.end.character > name_range.start.character);
let primary_usages = manager.get_usages("--primary-color").await;
assert_eq!(primary_usages.len(), 2);
for usage in &primary_usages {
assert!(usage.name_range.is_some());
let usage_name_range = usage.name_range.as_ref().unwrap();
assert!(usage_name_range.end.line >= usage_name_range.start.line);
assert!(usage_name_range.end.character > usage_name_range.start.character);
}
let (defs, usages) = manager.get_references("--primary-color").await;
assert_eq!(defs.len(), 1);
assert_eq!(usages.len(), 2);
assert!(defs[0].name_range.is_some());
for usage in &usages {
assert!(usage.name_range.is_some());
}
}
#[tokio::test]
async fn test_lsp_rename_preserves_fallbacks() {
let manager = CssVariableManager::new(Config::default());
let uri = Uri::from_str("file:///fallback-rename.css").unwrap();
let css_content = r#"
:root {
--primary-color: #3b82f6;
}
.button {
background: var(--primary-color, blue);
border: 1px solid var(--secondary-color, #ccc);
color: var(--primary-color);
}
.card {
background: var(--primary-color, var(--fallback-color, white));
}
"#;
parse_css_document(css_content, &uri, &manager)
.await
.unwrap();
let primary_usages = manager.get_usages("--primary-color").await;
assert_eq!(primary_usages.len(), 3);
let secondary_usages = manager.get_usages("--secondary-color").await;
assert_eq!(secondary_usages.len(), 1);
let fallback_usages = manager.get_usages("--fallback-color").await;
assert_eq!(fallback_usages.len(), 0);
let (defs, usages) = manager.get_references("--primary-color").await;
assert_eq!(defs.len(), 1);
assert_eq!(usages.len(), 3);
for usage in &usages {
assert!(usage.name_range.is_some());
let name_range = usage.name_range.as_ref().unwrap();
assert!(name_range.end.character > name_range.start.character);
}
}